Permalink
Browse files

Add a Cache interface, implemented by an in-memory cache and a memcac…

…hed client.

- The cache implementation may be configured from app.conf
- The cache handles serialization -- applications may cache any type (interface{})
- Serialization is done using gob encoding, except when storing []byte or ints
  • Loading branch information...
1 parent 1768509 commit 7363aa266cb104b2e47d44a53431d4cfbd62fc01 @robfig committed Feb 27, 2013
Showing with 781 additions and 0 deletions.
  1. +2 −0 .travis.yml
  2. +125 −0 cache/cache.go
  3. +206 −0 cache/cache_test.go
  4. +34 −0 cache/init.go
  5. +83 −0 cache/inmemory.go
  6. +36 −0 cache/inmemory_test.go
  7. +91 −0 cache/memcached.go
  8. +46 −0 cache/memcached_test.go
  9. +75 −0 cache/serialization.go
  10. +83 −0 cache/serialization_test.go
View
2 .travis.yml
@@ -1 +1,3 @@
language: go
+services:
+ - memcache # github.com/robfig/revel/cache
View
125 cache/cache.go
@@ -0,0 +1,125 @@
+package cache
+
+import (
+ "errors"
+ "time"
+)
+
+// Length of time to cache an item.
+const (
+ DEFAULT = time.Duration(0)
+ FOREVER = time.Duration(-1)
+)
+
+// Cache is an interface to an expiring cache. It behaves (and is modeled) like
+// the Memcached interface.
+//
+// Many callers will make exclusive use of Set and Get, but more exotic
+// functions are also available.
+//
+// Example
+//
+// Here is a typical Get/Set interaction:
+//
+// var items []*Item
+// if err := cache.Get("items", &items); err != nil {
+// items = loadItems()
+// go cache.Set("items", items, cache.DEFAULT)
+// }
+//
+// Note that the caller will frequently not wait for Set() to complete.
+//
+// Errors
+//
+// It is assumed that callers will infrequently check returned errors, since any
+// request should be fulfillable without finding anything in the cache. As a
+// result, all errors other than ErrCacheMiss and ErrNotStored will be logged to
+// revel.ERROR, so that the developer does not need to check the return value to
+// discover things like deserialization or connection errors.
+type Cache interface {
+ // Set the given key/value in the cache. overwriting any existing value
+ // associated with that key.
+ //
+ // Returns:
+ // - nil on success
+ // - an implementation specific error otherwise
+ Set(key string, value interface{}, expires time.Duration) error
+
+ // Get the content associated with the given key. decoding it into the given
+ // pointer.
+ //
+ // Returns:
+ // - nil if the value was successfully retrieved and ptrValue set
+ // - ErrCacheMiss if the value was not in the cache
+ // - an implementation specific error otherwise
+ Get(key string, ptrValue interface{}) error
+
+ // Delete the given key from the cache.
+ //
+ // Returns:
+ // - nil on a successful delete
+ // - ErrCacheMiss if the value was not in the cache
+ // - an implementation specific error otherwise
+ Delete(key string) error
+
+ // Add the given key/value to the cache ONLY IF the key does not already exist.
+ //
+ // Returns:
+ // - nil if the value was added to the cache
+ // - ErrNotStored if the key was already present in the cache
+ // - an implementation-specific error otherwise
+ Add(key string, value interface{}, expires time.Duration) error
+
+ // Set the given key/value in the cache ONLY IF the key already exists.
+ //
+ // Returns:
+ // - nil if the value was replaced
+ // - ErrNotStored if the key does not exist in the cache
+ // - an implementation specific error otherwise
+ Replace(key string, value interface{}, expires time.Duration) error
+
+ // Increment the value stored at the given key by the given amount.
+ // The value silently wraps around upon exceeding the uint64 range.
+ //
+ // Returns the new counter value if the operation was successful, or:
+ // - ErrCacheMiss if the key was not found in the cache
+ // - an implementation specific error otherwise
+ Increment(key string, n uint64) (newValue uint64, err error)
+
+ // Decrement the value stored at the given key by the given amount.
+ // The value is capped at 0 on underflow, with no error returned.
+ //
+ // Returns the new counter value if the operation was successful, or:
+ // - ErrCacheMiss if the key was not found in the cache
+ // - an implementation specific error otherwise
+ Decrement(key string, n uint64) (newValue uint64, err error)
+
+ // Expire all cache entries immediately.
+ // This is not implemented for the memcached cache (intentionally).
+ // Returns an implementation specific error if the operation failed.
+ Flush() error
+}
+
+var (
+ Instance Cache
+
+ ErrCacheMiss = errors.New("revel/cache: key not found.")
+ ErrNotStored = errors.New("revel/cache: not stored.")
+)
+
+// The package implements the Cache interface (as sugar).
+
+func Get(key string, ptrValue interface{}) error { return Instance.Get(key, ptrValue) }
+func Delete(key string) error { return Instance.Delete(key) }
+func Increment(key string, n uint64) (newValue uint64, err error) { return Instance.Increment(key, n) }
+func Decrement(key string, n uint64) (newValue uint64, err error) { return Instance.Decrement(key, n) }
+func Flush() error { return Instance.Flush() }
+func Set(key string, value interface{}, expires time.Duration) error {
+ return Instance.Set(key, value, expires)
+}
+func Add(key string, value interface{}, expires time.Duration) error {
+ return Instance.Add(key, value, expires)
+}
+func Replace(key string, value interface{}, expires time.Duration) error {
+ return Instance.Replace(key, value, expires)
+}
View
206 cache/cache_test.go
@@ -0,0 +1,206 @@
+package cache
+
+import (
+ "math"
+ "testing"
+ "time"
+)
+
+// Tests against a generic Cache interface.
+// They should pass for all implementations.
+type cacheFactory func(*testing.T, time.Duration) Cache
+
+// Test typical cache interactions
+func typicalGetSet(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ value := "foo"
+ if err = cache.Set("value", value, DEFAULT); err != nil {
+ t.Errorf("Error setting a value: %s", err)
+ }
+
+ value = ""
+ err = cache.Get("value", &value)
+ if err != nil {
+ t.Errorf("Error getting a value: %s", err)
+ }
+ if value != "foo" {
+ t.Errorf("Expected to get foo back, got %s", value)
+ }
+}
+
+// Test the increment-decrement cases
+func incrDecr(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ // Normal increment / decrement operation.
+ if err = cache.Set("int", 10, DEFAULT); err != nil {
+ t.Errorf("Error setting int: %s", err)
+ }
+ newValue, err := cache.Increment("int", 50)
+ if err != nil {
+ t.Errorf("Error incrementing int: %s", err)
+ }
+ if newValue != 60 {
+ t.Errorf("Expected 60, was %d", newValue)
+ }
+
+ if newValue, err = cache.Decrement("int", 50); err != nil {
+ t.Errorf("Error decrementing: %s", err)
+ }
+ if newValue != 10 {
+ t.Errorf("Expected 10, was %d", newValue)
+ }
+
+ // Increment wraparound
+ newValue, err = cache.Increment("int", math.MaxUint64-5)
+ if err != nil {
+ t.Errorf("Error wrapping around: %s", err)
+ }
+ if newValue != 4 {
+ t.Errorf("Expected wraparound 4, got %d", newValue)
+ }
+
+ // Decrement capped at 0
+ newValue, err = cache.Decrement("int", 25)
+ if err != nil {
+ t.Errorf("Error decrementing below 0: %s", err)
+ }
+ if newValue != 0 {
+ t.Errorf("Expected capped at 0, got %d", newValue)
+ }
+}
+
+func expiration(t *testing.T, newCache cacheFactory) {
+ // memcached does not support expiration times less than 1 second.
+ var err error
+ cache := newCache(t, time.Second)
+
+ // Test Set w/ DEFAULT
+ value := 10
+ cache.Set("int", value, DEFAULT)
+ time.Sleep(time.Second)
+ err = cache.Get("int", &value)
+ if err != ErrCacheMiss {
+ t.Errorf("Expected CacheMiss, but got: %s", err)
+ }
+
+ // Test Set w/ short time
+ cache.Set("int", value, time.Second)
+ time.Sleep(time.Second)
+ err = cache.Get("int", &value)
+ if err != ErrCacheMiss {
+ t.Errorf("Expected CacheMiss, but got: %s", err)
+ }
+
+ // Test Set w/ longer time.
+ cache.Set("int", value, time.Hour)
+ time.Sleep(time.Second)
+ err = cache.Get("int", &value)
+ if err != nil {
+ t.Errorf("Expected to get the value, but got: %s", err)
+ }
+
+ // Test Set w/ forever.
+ cache.Set("int", value, FOREVER)
+ time.Sleep(time.Second)
+ err = cache.Get("int", &value)
+ if err != nil {
+ t.Errorf("Expected to get the value, but got: %s", err)
+ }
+}
+
+func emptyCache(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ err = cache.Get("notexist", 0)
+ if err == nil {
+ t.Errorf("Error expected for non-existent key")
+ }
+ if err != ErrCacheMiss {
+ t.Errorf("Expected ErrNotExists for non-existent key: %s", err)
+ }
+
+ err = cache.Delete("notexist")
+ if err != ErrCacheMiss {
+ t.Errorf("Expected ErrNotExists for non-existent key: %s", err)
+ }
+
+ _, err = cache.Increment("notexist", 1)
+ if err != ErrCacheMiss {
+ t.Errorf("Expected cache miss incrementing non-existent key: %s", err)
+ }
+
+ _, err = cache.Decrement("notexist", 1)
+ if err != ErrCacheMiss {
+ t.Errorf("Expected cache miss decrementing non-existent key: %s", err)
+ }
+}
+
+func testReplace(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ // Replace in an empty cache.
+ if err = cache.Replace("notexist", 1, FOREVER); err != ErrNotStored {
+ t.Errorf("Replace in empty cache: expected ErrNotStored, got: %s", err)
+ }
+
+ // Set a value of 1, and replace it with 2
+ if err = cache.Set("int", 1, time.Second); err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ }
+
+ if err = cache.Replace("int", 2, time.Second); err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ }
+ var i int
+ if err = cache.Get("int", &i); err != nil {
+ t.Errorf("Unexpected error getting a replaced item: %s", err)
+ }
+ if i != 2 {
+ t.Errorf("Expected 2, got %d", i)
+ }
+
+ // Wait for it to expire and replace with 3 (unsuccessfully).
+ time.Sleep(time.Second)
+ if err = cache.Replace("int", 3, time.Second); err != ErrNotStored {
+ t.Errorf("Expected ErrNotStored, got: %s", err)
+ }
+ if err = cache.Get("int", &i); err != ErrCacheMiss {
+ t.Errorf("Expected cache miss, got: %s", err)
+ }
+}
+
+func testAdd(t *testing.T, newCache cacheFactory) {
+ var err error
+ cache := newCache(t, time.Hour)
+
+ // Add to an empty cache.
+ if err = cache.Add("int", 1, time.Second); err != nil {
+ t.Errorf("Unexpected error adding to empty cache: %s", err)
+ }
+
+ // Try to add again. (fail)
+ if err = cache.Add("int", 2, time.Second); err != ErrNotStored {
+ t.Errorf("Expected ErrNotStored adding dupe to cache: %s", err)
+ }
+
+ // Wait for it to expire, and add again.
+ time.Sleep(time.Second)
+ if err = cache.Add("int", 3, time.Second); err != nil {
+ t.Errorf("Unexpected error adding to cache: %s", err)
+ }
+
+ // Get and verify the value.
+ var i int
+ if err = cache.Get("int", &i); err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ }
+ if i != 3 {
+ t.Errorf("Expected 3, got: %d", i)
+ }
+}
View
34 cache/init.go
@@ -0,0 +1,34 @@
+package cache
+
+import (
+ "github.com/robfig/revel"
+ "strings"
+ "time"
+)
+
+func init() {
+ revel.OnAppStart(func() {
+ // Set the default expiration time.
+ defaultExpiration := time.Hour // The default for the default is one hour.
+ if expireStr, found := revel.Config.String("cache.expires"); found {
+ var err error
+ if defaultExpiration, err = time.ParseDuration(expireStr); err != nil {
+ panic("Could not parse default cache expiration duration " + expireStr + ": " + err.Error())
+ }
+ }
+
+ // Use memcached?
+ if revel.Config.BoolDefault("cache.memcached", false) {
+ hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",")
+ if len(hosts) == 0 {
+ panic("Memcache enabled but no memcached hosts specified!")
+ }
+
+ Instance = NewMemcachedCache(hosts, defaultExpiration)
+ return
+ }
+
+ // By default, use the in-memory cache.
+ Instance = NewInMemoryCache(defaultExpiration)
+ })
+}
View
83 cache/inmemory.go
@@ -0,0 +1,83 @@
+package cache
+
+import (
+ "fmt"
+ "github.com/robfig/go-cache"
+ "github.com/robfig/revel"
+ "reflect"
+ "time"
+)
+
+type InMemoryCache struct {
+ cache.Cache
+}
+
+func NewInMemoryCache(defaultExpiration time.Duration) InMemoryCache {
+ return InMemoryCache{*cache.New(defaultExpiration, time.Minute)}
+}
+
+func (c InMemoryCache) Get(key string, ptrValue interface{}) error {
+ value, found := c.Cache.Get(key)
+ if !found {
+ return ErrCacheMiss
+ }
+
+ v := reflect.ValueOf(ptrValue)
+ if v.Type().Kind() == reflect.Ptr && v.Elem().CanSet() {
+ v.Elem().Set(reflect.ValueOf(value))
+ return nil
+ }
+
+ err := fmt.Errorf("revel/cache: attempt to get %s, but can not set value %v", key, v)
+ revel.ERROR.Println(err)
+ return err
+}
+
+func (c InMemoryCache) Set(key string, value interface{}, expires time.Duration) error {
+ // NOTE: go-cache understands the values of DEFAULT and FOREVER
+ c.Cache.Set(key, value, expires)
+ return nil
+}
+
+func (c InMemoryCache) Add(key string, value interface{}, expires time.Duration) error {
+ err := c.Cache.Add(key, value, expires)
+ if err == cache.ErrKeyExists {
+ return ErrNotStored
+ }
+ return err
+}
+
+func (c InMemoryCache) Replace(key string, value interface{}, expires time.Duration) error {
+ if err := c.Cache.Replace(key, value, expires); err != nil {
+ return ErrNotStored
+ }
+ return nil
+}
+
+func (c InMemoryCache) Delete(key string) error {
+ if found := c.Cache.Delete(key); !found {
+ return ErrCacheMiss
+ }
+ return nil
+}
+
+func (c InMemoryCache) Increment(key string, n uint64) (newValue uint64, err error) {
+ newValue, err = c.Cache.Increment(key, n)
+ if err == cache.ErrCacheMiss {
+ return 0, ErrCacheMiss
+ }
+ return
+}
+
+func (c InMemoryCache) Decrement(key string, n uint64) (newValue uint64, err error) {
+ newValue, err = c.Cache.Decrement(key, n)
+ if err == cache.ErrCacheMiss {
+ return 0, ErrCacheMiss
+ }
+ return
+}
+
+func (c InMemoryCache) Flush() error {
+ c.Cache.Flush()
+ return nil
+}
View
36 cache/inmemory_test.go
@@ -0,0 +1,36 @@
+package cache
+
+import (
+ "testing"
+ "time"
+)
+
+var newInMemoryCache = func(_ *testing.T, defaultExpiration time.Duration) Cache {
+ return NewInMemoryCache(defaultExpiration)
+}
+
+// Test typical cache interactions
+func TestInMemoryCache_TypicalGetSet(t *testing.T) {
+ typicalGetSet(t, newInMemoryCache)
+}
+
+// Test the increment-decrement cases
+func TestInMemoryCache_IncrDecr(t *testing.T) {
+ incrDecr(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Expiration(t *testing.T) {
+ expiration(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_EmptyCache(t *testing.T) {
+ emptyCache(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Replace(t *testing.T) {
+ testReplace(t, newInMemoryCache)
+}
+
+func TestInMemoryCache_Add(t *testing.T) {
+ testAdd(t, newInMemoryCache)
+}
View
91 cache/memcached.go
@@ -0,0 +1,91 @@
+package cache
+
+import (
+ "errors"
+ "github.com/robfig/gomemcache/memcache"
+ "github.com/robfig/revel"
+ "time"
+)
+
+// Wraps the Memcached client to meet the Cache interface.
+type MemcachedCache struct {
+ *memcache.Client
+ defaultExpiration time.Duration
+}
+
+func NewMemcachedCache(hostList []string, defaultExpiration time.Duration) MemcachedCache {
+ return MemcachedCache{memcache.New(hostList...), defaultExpiration}
+}
+
+func (c MemcachedCache) Set(key string, value interface{}, expires time.Duration) error {
+ return c.invoke((*memcache.Client).Set, key, value, expires)
+}
+
+func (c MemcachedCache) Add(key string, value interface{}, expires time.Duration) error {
+ return c.invoke((*memcache.Client).Add, key, value, expires)
+}
+
+func (c MemcachedCache) Replace(key string, value interface{}, expires time.Duration) error {
+ return c.invoke((*memcache.Client).Replace, key, value, expires)
+}
+
+func (c MemcachedCache) Get(key string, ptrValue interface{}) error {
+ item, err := c.Client.Get(key)
+ if err != nil {
+ return convertMemcacheError(err)
+ }
+ return Deserialize(item.Value, ptrValue)
+}
+
+func (c MemcachedCache) Delete(key string) error {
+ return convertMemcacheError(c.Client.Delete(key))
+}
+
+func (c MemcachedCache) Increment(key string, delta uint64) (newValue uint64, err error) {
+ newValue, err = c.Client.Increment(key, delta)
+ return newValue, convertMemcacheError(err)
+}
+
+func (c MemcachedCache) Decrement(key string, delta uint64) (newValue uint64, err error) {
+ newValue, err = c.Client.Decrement(key, delta)
+ return newValue, convertMemcacheError(err)
+}
+
+func (c MemcachedCache) Flush() error {
+ err := errors.New("revel/cache: can not flush memcached.")
+ revel.ERROR.Println(err)
+ return err
+}
+
+func (c MemcachedCache) invoke(f func(*memcache.Client, *memcache.Item) error,
+ key string, value interface{}, expires time.Duration) error {
+
+ switch expires {
+ case DEFAULT:
+ expires = c.defaultExpiration
+ case FOREVER:
+ expires = time.Duration(0)
+ }
+
+ b, err := Serialize(value)
+ if err != nil {
+ return err
+ }
+ return convertMemcacheError(f(c.Client, &memcache.Item{
+ Key: key,
+ Value: b,
+ Expiration: int32(expires / time.Second),
+ }))
+}
+
+func convertMemcacheError(err error) error {
+ switch err {
+ case memcache.ErrCacheMiss:
+ return ErrCacheMiss
+ case memcache.ErrNotStored:
+ return ErrNotStored
+ }
+
+ revel.ERROR.Printf("revel/cache: %s", err)
+ return err
+}
View
46 cache/memcached_test.go
@@ -0,0 +1,46 @@
+package cache
+
+import (
+ "net"
+ "testing"
+ "time"
+)
+
+// These tests require memcached running on localhost:11211 (the default)
+const testServer = "localhost:11211"
+
+var newMemcachedCache = func(t *testing.T, defaultExpiration time.Duration) Cache {
+ c, err := net.Dial("tcp", testServer)
+ if err == nil {
+ c.Write([]byte("flush_all\r\n"))
+ c.Close()
+ return NewMemcachedCache([]string{testServer}, defaultExpiration)
+ }
+ t.Errorf("couldn't connect to memcached on %s", testServer)
+ t.FailNow()
+ panic("")
+}
+
+func TestMemcachedCache_TypicalGetSet(t *testing.T) {
+ typicalGetSet(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_IncrDecr(t *testing.T) {
+ incrDecr(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Expiration(t *testing.T) {
+ expiration(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_EmptyCache(t *testing.T) {
+ emptyCache(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Replace(t *testing.T) {
+ testReplace(t, newMemcachedCache)
+}
+
+func TestMemcachedCache_Add(t *testing.T) {
+ testAdd(t, newMemcachedCache)
+}
View
75 cache/serialization.go
@@ -0,0 +1,75 @@
+package cache
+
+import (
+ "bytes"
+ "encoding/gob"
+ "github.com/robfig/revel"
+ "reflect"
+ "strconv"
+)
+
+// Serialize transforms the given value into bytes following these rules:
+// - If value is a byte array, it is returned as-is.
+// - If value is an int or uint type, it is returned as the ASCII representation
+// - Else, encoding/gob is used to serialize
+func Serialize(value interface{}) ([]byte, error) {
+ if bytes, ok := value.([]byte); ok {
+ return bytes, nil
+ }
+
+ switch v := reflect.ValueOf(value); v.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return []byte(strconv.FormatInt(v.Int(), 10)), nil
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return []byte(strconv.FormatUint(v.Uint(), 10)), nil
+ }
+
+ var b bytes.Buffer
+ encoder := gob.NewEncoder(&b)
+ if err := encoder.Encode(value); err != nil {
+ revel.ERROR.Printf("revel/cache: gob encoding '%s' failed: %s", value, err)
+ return nil, err
+ }
+ return b.Bytes(), nil
+}
+
+// Deserialize transforms bytes produced by Serialize back into a Go object,
+// storing it into "ptr", which must be a pointer to the value type.
+func Deserialize(byt []byte, ptr interface{}) (err error) {
+ if bytes, ok := ptr.(*[]byte); ok {
+ *bytes = byt
+ return
+ }
+
+ if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr {
+ switch p := v.Elem(); p.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ var i int64
+ i, err = strconv.ParseInt(string(byt), 10, 64)
+ if err != nil {
+ revel.ERROR.Printf("revel/cache: failed to parse int '%s': %s", string(byt), err)
+ } else {
+ p.SetInt(i)
+ }
+ return
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ var i uint64
+ i, err = strconv.ParseUint(string(byt), 10, 64)
+ if err != nil {
+ revel.ERROR.Printf("revel/cache: failed to parse uint '%s': %s", string(byt), err)
+ } else {
+ p.SetUint(i)
+ }
+ return
+ }
+ }
+
+ b := bytes.NewBuffer(byt)
+ decoder := gob.NewDecoder(b)
+ if err = decoder.Decode(ptr); err != nil {
+ revel.ERROR.Printf("revel/cache: gob decoding failed: %s", err)
+ return
+ }
+ return
+}
View
83 cache/serialization_test.go
@@ -0,0 +1,83 @@
+package cache
+
+import (
+ "reflect"
+ "testing"
+)
+
+type Struct1 struct {
+ X int
+}
+
+func (s Struct1) Method1() {}
+
+type Interface1 interface {
+ Method1()
+}
+
+var (
+ struct1 Struct1 = Struct1{1}
+ ptrStruct *Struct1 = &Struct1{2}
+ emptyIface interface{} = Struct1{3}
+ iface1 Interface1 = Struct1{4}
+ sliceStruct []Struct1 = []Struct1{{5}, {6}, {7}}
+ ptrSliceStruct []*Struct1 = []*Struct1{{8}, {9}, {10}}
+
+ VALUE_MAP = map[string]interface{}{
+ "bytes": []byte{0x61, 0x62, 0x63, 0x64},
+ "string": "string",
+ "bool": true,
+ "int": 5,
+ "int8": int8(5),
+ "int16": int16(5),
+ "int32": int32(5),
+ "int64": int64(5),
+ "uint": uint(5),
+ "uint8": uint8(5),
+ "uint16": uint16(5),
+ "uint32": uint32(5),
+ "uint64": uint64(5),
+ "float32": float32(5),
+ "float64": float64(5),
+ "array": [5]int{1, 2, 3, 4, 5},
+ "slice": []int{1, 2, 3, 4, 5},
+ "emptyIf": emptyIface,
+ "Iface1": iface1,
+ "map": map[string]string{"foo": "bar"},
+ "ptrStruct": ptrStruct,
+ "struct1": struct1,
+ "sliceStruct": sliceStruct,
+ "ptrSliceStruct": ptrSliceStruct,
+ }
+)
+
+// Test passing all kinds of data between serialize and deserialize.
+func TestRoundTrip(t *testing.T) {
+ for _, expected := range VALUE_MAP {
+ bytes, err := Serialize(expected)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+
+ ptrActual := reflect.New(reflect.TypeOf(expected)).Interface()
+ err = Deserialize(bytes, ptrActual)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+
+ actual := reflect.ValueOf(ptrActual).Elem().Interface()
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("(expected) %T %v != %T %v (actual)", expected, expected, actual, actual)
+ }
+ }
+}
+
+func zeroMap(arg map[string]interface{}) map[string]interface{} {
+ result := map[string]interface{}{}
+ for key, value := range arg {
+ result[key] = reflect.Zero(reflect.TypeOf(value)).Interface()
+ }
+ return result
+}

0 comments on commit 7363aa2

Please sign in to comment.