familia 1.0.0.pre.rc7 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.pre-commit-config.yaml +1 -1
  4. data/Gemfile +3 -1
  5. data/Gemfile.lock +3 -1
  6. data/README.md +5 -5
  7. data/VERSION.yml +1 -2
  8. data/familia.gemspec +1 -2
  9. data/lib/familia/base.rb +11 -11
  10. data/lib/familia/connection.rb +18 -2
  11. data/lib/familia/errors.rb +14 -0
  12. data/lib/familia/features/expiration.rb +13 -2
  13. data/lib/familia/features/quantization.rb +1 -1
  14. data/lib/familia/horreum/class_methods.rb +31 -21
  15. data/lib/familia/horreum/commands.rb +58 -15
  16. data/lib/familia/horreum/relations_management.rb +9 -2
  17. data/lib/familia/horreum/serialization.rb +130 -29
  18. data/lib/familia/horreum.rb +69 -19
  19. data/lib/familia/redistype/commands.rb +5 -2
  20. data/lib/familia/redistype/serialization.rb +17 -16
  21. data/lib/familia/redistype/types/hashkey.rb +167 -0
  22. data/lib/familia/{types → redistype/types}/list.rb +19 -14
  23. data/lib/familia/{types → redistype/types}/sorted_set.rb +22 -19
  24. data/lib/familia/{types → redistype/types}/string.rb +8 -6
  25. data/lib/familia/{types → redistype/types}/unsorted_set.rb +16 -12
  26. data/lib/familia/redistype.rb +19 -9
  27. data/lib/familia.rb +6 -1
  28. data/try/10_familia_try.rb +1 -1
  29. data/try/20_redis_type_try.rb +1 -1
  30. data/try/21_redis_type_zset_try.rb +1 -1
  31. data/try/22_redis_type_set_try.rb +1 -1
  32. data/try/23_redis_type_list_try.rb +2 -2
  33. data/try/24_redis_type_string_try.rb +3 -3
  34. data/try/25_redis_type_hash_try.rb +1 -1
  35. data/try/26_redis_bool_try.rb +2 -2
  36. data/try/27_redis_horreum_try.rb +2 -2
  37. data/try/28_redis_horreum_serialization_try.rb +159 -0
  38. data/try/29_redis_horreum_initialization_try.rb +113 -0
  39. data/try/30_familia_object_try.rb +8 -5
  40. data/try/40_customer_try.rb +6 -6
  41. data/try/91_json_bug_try.rb +86 -0
  42. data/try/92_symbolize_try.rb +96 -0
  43. data/try/93_string_coercion_try.rb +154 -0
  44. metadata +20 -15
  45. data/lib/familia/types/hashkey.rb +0 -108
@@ -2,18 +2,21 @@
2
2
 
3
3
  module Familia
4
4
  class List < RedisType
5
- def size
5
+
6
+ # Returns the number of elements in the list
7
+ # @return [Integer] number of elements
8
+ def element_count
6
9
  redis.llen rediskey
7
10
  end
8
- alias length size
11
+ alias size element_count
9
12
 
10
13
  def empty?
11
- size.zero?
14
+ element_count.zero?
12
15
  end
13
16
 
14
17
  def push *values
15
18
  echo :push, caller(1..1).first if Familia.debug
16
- values.flatten.compact.each { |v| redis.rpush rediskey, to_redis(v) }
19
+ values.flatten.compact.each { |v| redis.rpush rediskey, serialize_value(v) }
17
20
  redis.ltrim rediskey, -@opts[:maxlength], -1 if @opts[:maxlength]
18
21
  update_expiration
19
22
  self
@@ -26,7 +29,7 @@ module Familia
26
29
  alias add <<
27
30
 
28
31
  def unshift *values
29
- values.flatten.compact.each { |v| redis.lpush rediskey, to_redis(v) }
32
+ values.flatten.compact.each { |v| redis.lpush rediskey, serialize_value(v) }
30
33
  # TODO: test maxlength
31
34
  redis.ltrim rediskey, 0, @opts[:maxlength] - 1 if @opts[:maxlength]
32
35
  update_expiration
@@ -35,11 +38,11 @@ module Familia
35
38
  alias prepend unshift
36
39
 
37
40
  def pop
38
- from_redis redis.rpop(rediskey)
41
+ deserialize_value redis.rpop(rediskey)
39
42
  end
40
43
 
41
44
  def shift
42
- from_redis redis.lpop(rediskey)
45
+ deserialize_value redis.lpop(rediskey)
43
46
  end
44
47
 
45
48
  def [](idx, count = nil)
@@ -57,16 +60,18 @@ module Familia
57
60
  end
58
61
  alias slice []
59
62
 
60
- def delete(v, count = 0)
61
- redis.lrem rediskey, count, to_redis(v)
63
+ # Removes elements equal to value from the list
64
+ # @param value The value to remove
65
+ # @param count [Integer] Number of elements to remove (0 means all)
66
+ # @return [Integer] The number of removed elements
67
+ def remove_element(value, count = 0)
68
+ redis.lrem rediskey, count, serialize_value(value)
62
69
  end
63
- alias remove delete
64
- alias rem delete
65
- alias del delete
70
+ alias remove remove_element
66
71
 
67
72
  def range(sidx = 0, eidx = -1)
68
73
  elements = rangeraw sidx, eidx
69
- multi_from_redis(*elements)
74
+ deserialize_values(*elements)
70
75
  end
71
76
 
72
77
  def rangeraw(sidx = 0, eidx = -1)
@@ -119,7 +124,7 @@ module Familia
119
124
  end
120
125
 
121
126
  def at(idx)
122
- from_redis redis.lindex(rediskey, idx)
127
+ deserialize_value redis.lindex(rediskey, idx)
123
128
  end
124
129
 
125
130
  def first
@@ -2,13 +2,15 @@
2
2
 
3
3
  module Familia
4
4
  class SortedSet < RedisType
5
- def size
5
+ # Returns the number of elements in the sorted set
6
+ # @return [Integer] number of elements
7
+ def element_count
6
8
  redis.zcard rediskey
7
9
  end
8
- alias length size
10
+ alias size element_count
9
11
 
10
12
  def empty?
11
- size.zero?
13
+ element_count.zero?
12
14
  end
13
15
 
14
16
  # Adds a new element to the sorted set with the current timestamp as the
@@ -44,13 +46,13 @@ module Familia
44
46
  end
45
47
 
46
48
  def add(score, val)
47
- ret = redis.zadd rediskey, score, to_redis(val)
49
+ ret = redis.zadd rediskey, score, serialize_value(val)
48
50
  update_expiration
49
51
  ret
50
52
  end
51
53
 
52
54
  def score(val)
53
- ret = redis.zscore rediskey, to_redis(val, strict_values: false)
55
+ ret = redis.zscore rediskey, serialize_value(val, strict_values: false)
54
56
  ret&.to_f
55
57
  end
56
58
  alias [] score
@@ -63,20 +65,20 @@ module Familia
63
65
 
64
66
  # rank of member +v+ when ordered lowest to highest (starts at 0)
65
67
  def rank(v)
66
- ret = redis.zrank rediskey, to_redis(v, strict_values: false)
68
+ ret = redis.zrank rediskey, serialize_value(v, strict_values: false)
67
69
  ret&.to_i
68
70
  end
69
71
 
70
72
  # rank of member +v+ when ordered highest to lowest (starts at 0)
71
73
  def revrank(v)
72
- ret = redis.zrevrank rediskey, to_redis(v, strict_values: false)
74
+ ret = redis.zrevrank rediskey, serialize_value(v, strict_values: false)
73
75
  ret&.to_i
74
76
  end
75
77
 
76
78
  def members(count = -1, opts = {})
77
79
  count -= 1 if count.positive?
78
80
  elements = membersraw count, opts
79
- multi_from_redis(*elements)
81
+ deserialize_values(*elements)
80
82
  end
81
83
  alias to_a members
82
84
  alias all members
@@ -89,7 +91,7 @@ module Familia
89
91
  def revmembers(count = -1, opts = {})
90
92
  count -= 1 if count.positive?
91
93
  elements = revmembersraw count, opts
92
- multi_from_redis(*elements)
94
+ deserialize_values(*elements)
93
95
  end
94
96
 
95
97
  def revmembersraw(count = -1, opts = {})
@@ -132,7 +134,7 @@ module Familia
132
134
  def range(sidx, eidx, opts = {})
133
135
  echo :range, caller(1..1).first if Familia.debug
134
136
  elements = rangeraw(sidx, eidx, opts)
135
- multi_from_redis(*elements)
137
+ deserialize_values(*elements)
136
138
  end
137
139
 
138
140
  def rangeraw(sidx, eidx, opts = {})
@@ -149,7 +151,7 @@ module Familia
149
151
  def revrange(sidx, eidx, opts = {})
150
152
  echo :revrange, caller(1..1).first if Familia.debug
151
153
  elements = revrangeraw(sidx, eidx, opts)
152
- multi_from_redis(*elements)
154
+ deserialize_values(*elements)
153
155
  end
154
156
 
155
157
  def revrangeraw(sidx, eidx, opts = {})
@@ -160,7 +162,7 @@ module Familia
160
162
  def rangebyscore(sscore, escore, opts = {})
161
163
  echo :rangebyscore, caller(1..1).first if Familia.debug
162
164
  elements = rangebyscoreraw(sscore, escore, opts)
163
- multi_from_redis(*elements)
165
+ deserialize_values(*elements)
164
166
  end
165
167
 
166
168
  def rangebyscoreraw(sscore, escore, opts = {})
@@ -172,7 +174,7 @@ module Familia
172
174
  def revrangebyscore(sscore, escore, opts = {})
173
175
  echo :revrangebyscore, caller(1..1).first if Familia.debug
174
176
  elements = revrangebyscoreraw(sscore, escore, opts)
175
- multi_from_redis(*elements)
177
+ deserialize_values(*elements)
176
178
  end
177
179
 
178
180
  def revrangebyscoreraw(sscore, escore, opts = {})
@@ -201,18 +203,19 @@ module Familia
201
203
  alias decr decrement
202
204
  alias decrby decrement
203
205
 
204
- def delete(val)
205
- Familia.trace :DELETE, redis, "#{val}<#{val.class}>", caller(1..1) if Familia.debug?
206
+ # Removes a member from the sorted set
207
+ # @param value The value to remove from the sorted set
208
+ # @return [Integer] The number of members that were removed (0 or 1)
209
+ def remove_element(value)
210
+ Familia.trace :REMOVE_ELEMENT, redis, "#{value}<#{value.class}>", caller(1..1) if Familia.debug?
206
211
  # We use `strict_values: false` here to allow for the deletion of values
207
212
  # that are in the sorted set. If it's a horreum object, the value is
208
213
  # the identifier and not a serialized version of the object. So either
209
214
  # the value exists in the sorted set or it doesn't -- we don't need to
210
215
  # raise an error if it's not found.
211
- redis.zrem rediskey, to_redis(val, strict_values: false)
216
+ redis.zrem rediskey, serialize_value(value, strict_values: false)
212
217
  end
213
- alias remove delete
214
- alias rem delete
215
- alias del delete
218
+ alias remove remove_element # deprecated
216
219
 
217
220
  def at(idx)
218
221
  range(idx, idx).first
@@ -4,19 +4,21 @@ module Familia
4
4
  class String < RedisType
5
5
  def init; end
6
6
 
7
- def size
7
+ # Returns the number of elements in the list
8
+ # @return [Integer] number of elements
9
+ def char_count
8
10
  to_s.size
9
11
  end
10
- alias length size
12
+ alias size char_count
11
13
 
12
14
  def empty?
13
- size.zero?
15
+ char_count.zero?
14
16
  end
15
17
 
16
18
  def value
17
19
  echo :value, caller(0..0) if Familia.debug
18
20
  redis.setnx rediskey, @opts[:default] if @opts[:default]
19
- from_redis redis.get(rediskey)
21
+ deserialize_value redis.get(rediskey)
20
22
  end
21
23
  alias content value
22
24
  alias get value
@@ -30,7 +32,7 @@ module Familia
30
32
  end
31
33
 
32
34
  def value=(val)
33
- ret = redis.set(rediskey, to_redis(val))
35
+ ret = redis.set(rediskey, serialize_value(val))
34
36
  update_expiration
35
37
  ret
36
38
  end
@@ -38,7 +40,7 @@ module Familia
38
40
  alias set value=
39
41
 
40
42
  def setnx(val)
41
- ret = redis.setnx(rediskey, to_redis(val))
43
+ ret = redis.setnx(rediskey, serialize_value(val))
42
44
  update_expiration
43
45
  ret
44
46
  end
@@ -2,17 +2,20 @@
2
2
 
3
3
  module Familia
4
4
  class Set < RedisType
5
- def size
5
+
6
+ # Returns the number of elements in the unsorted set
7
+ # @return [Integer] number of elements
8
+ def element_count
6
9
  redis.scard rediskey
7
10
  end
8
- alias length size
11
+ alias size element_count
9
12
 
10
13
  def empty?
11
- size.zero?
14
+ element_count.zero?
12
15
  end
13
16
 
14
17
  def add *values
15
- values.flatten.compact.each { |v| redis.sadd? rediskey, to_redis(v) }
18
+ values.flatten.compact.each { |v| redis.sadd? rediskey, serialize_value(v) }
16
19
  update_expiration
17
20
  self
18
21
  end
@@ -24,7 +27,7 @@ module Familia
24
27
  def members
25
28
  echo :members, caller(1..1).first if Familia.debug
26
29
  elements = membersraw
27
- multi_from_redis(*elements)
30
+ deserialize_values(*elements)
28
31
  end
29
32
  alias all members
30
33
  alias to_a members
@@ -66,16 +69,17 @@ module Familia
66
69
  end
67
70
 
68
71
  def member?(val)
69
- redis.sismember rediskey, to_redis(val)
72
+ redis.sismember rediskey, serialize_value(val)
70
73
  end
71
74
  alias include? member?
72
75
 
73
- def delete(val)
74
- redis.srem rediskey, to_redis(val)
76
+ # Removes a member from the set
77
+ # @param value The value to remove from the set
78
+ # @return [Integer] The number of members that were removed (0 or 1)
79
+ def remove_element(value)
80
+ redis.srem rediskey, serialize_value(value)
75
81
  end
76
- alias remove delete
77
- alias rem delete
78
- alias del delete
82
+ alias remove remove_element # deprecated
79
83
 
80
84
  def intersection *setkeys
81
85
  # TODO
@@ -90,7 +94,7 @@ module Familia
90
94
  end
91
95
 
92
96
  def random
93
- from_redis randomraw
97
+ deserialize_value randomraw
94
98
  end
95
99
 
96
100
  def randomraw
@@ -16,17 +16,19 @@ module Familia
16
16
  extend Familia::Features
17
17
 
18
18
  @registered_types = {}
19
- @valid_options = %i[class parent ttl default db key redis suffix]
19
+ @valid_options = %i[class parent ttl default db key redis suffix prefix]
20
20
  @db = nil
21
21
 
22
22
  feature :expiration
23
23
  feature :quantization
24
24
 
25
25
  class << self
26
- attr_reader :registered_types, :valid_options
26
+ attr_reader :registered_types, :valid_options, :has_relations
27
27
  attr_accessor :parent
28
28
  attr_writer :db, :uri
29
+ end
29
30
 
31
+ module ClassMethods
30
32
  # To be called inside every class that inherits RedisType
31
33
  # +methname+ is the term used for the class and instance methods
32
34
  # that are created for the given +klass+ (e.g. set, list, etc)
@@ -58,7 +60,12 @@ module Familia
58
60
  def valid_keys_only(opts)
59
61
  opts.select { |k, _| RedisType.valid_options.include? k }
60
62
  end
63
+
64
+ def has_relations?
65
+ @has_relations ||= false
66
+ end
61
67
  end
68
+ extend ClassMethods
62
69
 
63
70
  attr_reader :keystring, :parent, :opts
64
71
  attr_writer :dump_method, :load_method
@@ -89,8 +96,11 @@ module Familia
89
96
  # :key => a hardcoded key to use instead of the deriving the from
90
97
  # the name and parent (e.g. a derived key: customer:custid:secret_counter).
91
98
  #
92
- # Uses the redis connection of the parent or the value of
93
- # opts[:redis] or Familia.redis (in that order).
99
+ # :suffix => the suffix to use for the key (e.g. 'scores' in customer:custid:scores).
100
+ # :prefix => the prefix to use for the key (e.g. 'customer' in customer:custid:scores).
101
+ #
102
+ # Connection precendence: uses the redis connection of the parent or the
103
+ # value of opts[:redis] or Familia.redis (in that order).
94
104
  def initialize(keystring, opts = {})
95
105
  #Familia.ld " [initializing] #{self.class} #{opts}"
96
106
  @keystring = keystring
@@ -210,9 +220,9 @@ module Familia
210
220
  include Serialization
211
221
  end
212
222
 
213
- require_relative 'types/list'
214
- require_relative 'types/unsorted_set'
215
- require_relative 'types/sorted_set'
216
- require_relative 'types/hashkey'
217
- require_relative 'types/string'
223
+ require_relative 'redistype/types/list'
224
+ require_relative 'redistype/types/unsorted_set'
225
+ require_relative 'redistype/types/sorted_set'
226
+ require_relative 'redistype/types/hashkey'
227
+ require_relative 'redistype/types/string'
218
228
  end
data/lib/familia.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # rubocop:disable all
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'json'
4
5
  require 'redis'
5
6
  require 'uri/redis'
6
7
 
@@ -35,9 +36,13 @@ module Familia
35
36
  @members = []
36
37
 
37
38
  class << self
38
- attr_accessor :debug
39
+ attr_writer :debug
39
40
  attr_reader :members
40
41
 
42
+ def debug
43
+ @debug ||= ENV['FAMILIA_DEBUG'].to_s.match?(/^(true|1)$/i)
44
+ end
45
+
41
46
  def included(member)
42
47
  raise Problem, "#{member} should subclass Familia::Horreum"
43
48
  end
@@ -21,7 +21,7 @@ Familia::Horreum::ClassMethods.public_method_defined? :list
21
21
  Familia::Horreum::ClassMethods.public_method_defined? :lists
22
22
  #=> true
23
23
 
24
- ## A Familia object knows its redistype relativess
24
+ ## A Familia object knows its redistype relatives
25
25
  Bone.redis_types.is_a?(Hash) && Bone.redis_types.has_key?(:owners)
26
26
  #=> true
27
27
 
@@ -46,7 +46,7 @@ p [@limiter1.counter.parent.ttl, @limiter2.counter.parent.ttl]
46
46
  ##=> "v1:limiter:requests:counter:1302468600"
47
47
 
48
48
  ## Increment counter
49
- @limiter1.counter.clear
49
+ @limiter1.counter.delete!
50
50
  @limiter1.counter.increment
51
51
  #=> 1
52
52
 
@@ -65,4 +65,4 @@ require_relative './test_helpers'
65
65
  @a.metrics.members
66
66
  #=> ['metric2']
67
67
 
68
- @a.metrics.clear
68
+ @a.metrics.delete!
@@ -31,4 +31,4 @@ ret.class
31
31
  #=> 3
32
32
 
33
33
 
34
- @a.tags.clear
34
+ @a.tags.delete!
@@ -31,11 +31,11 @@ ret.class
31
31
  #=> ['value1','value2','value3']
32
32
 
33
33
  ## Familia::List#delete
34
- @a.owners.delete 'value3'
34
+ @a.owners.remove 'value3'
35
35
  #=> 1
36
36
 
37
37
  ## Familia::List#size
38
38
  @a.owners.size
39
39
  #=> 2
40
40
 
41
- @a.owners.clear
41
+ @a.owners.delete!
@@ -22,8 +22,8 @@ require_relative './test_helpers'
22
22
  #=> 'DECENT!'
23
23
 
24
24
  ## Familia::String#destroy!
25
- @a.value.clear
26
- #=> 1
25
+ @a.value.delete!
26
+ #=> true
27
27
 
28
28
  ## Familia::String.new
29
29
  @ret = Familia::String.new 'arbitrary:key'
@@ -63,4 +63,4 @@ require_relative './test_helpers'
63
63
  #=> '1050bytes'
64
64
 
65
65
 
66
- @ret.clear
66
+ @ret.delete!
@@ -50,4 +50,4 @@ require_relative './test_helpers'
50
50
  #=> ['1', '40', '3']
51
51
 
52
52
 
53
- @a.props.clear
53
+ @a.props.delete!
@@ -39,5 +39,5 @@ end
39
39
  #=> "true"
40
40
 
41
41
  ## Clear the hash key
42
- @hashkey.clear
43
- #=> 1
42
+ @hashkey.delete!
43
+ #=> true
@@ -36,8 +36,8 @@ Familia.debug = false
36
36
  #=> @identifier
37
37
 
38
38
  ## Remove the key
39
- @hashkey.clear
40
- #=> 1
39
+ @hashkey.delete!
40
+ #=> true
41
41
 
42
42
  ## Horreum objects can update and save their fields (1 of 2)
43
43
  @customer.name = 'John Doe'
@@ -0,0 +1,159 @@
1
+ require_relative '../lib/familia'
2
+ require_relative './test_helpers'
3
+
4
+ Familia.debug = false
5
+
6
+ @identifier = 'tryouts-28@onetimesecret.com'
7
+ @customer = Customer.new @identifier
8
+
9
+ ## Basic save functionality works
10
+ @customer.name = 'John Doe'
11
+ @customer.save
12
+ #=> true
13
+
14
+ ## to_h returns field hash with all Customer fields
15
+ @customer.to_h.class
16
+ #=> Hash
17
+
18
+ ## to_h includes the fields we set (using symbol keys)
19
+ @customer.to_h[:name]
20
+ #=> "John Doe"
21
+
22
+ ## to_h includes the key field (using symbol keys)
23
+ @customer.to_h[:key]
24
+ #=> "tryouts-28@onetimesecret.com"
25
+
26
+ ## to_a returns field array in definition order
27
+ @customer.to_a.class
28
+ #=> Array
29
+
30
+ ## to_a includes values in field order (name should be at index 5)
31
+ @customer.to_a[5]
32
+ #=> "John Doe"
33
+
34
+ ## batch_update can update multiple fields atomically, to_h
35
+ @result = @customer.batch_update(name: 'Jane Smith', email: 'jane@example.com')
36
+ @result.to_h
37
+ #=> {:success=>true, :results=>[0, 0]}
38
+
39
+ ## batch_update returns successful result, successful?
40
+ @result.successful?
41
+ #=> true
42
+
43
+ ## batch_update returns successful result, tuple
44
+ @result.tuple
45
+ #=> [true, [0, 0]]
46
+
47
+ ## batch_update returns successful result, to_a
48
+ @result.to_a
49
+ #=> [true, [0, 0]]
50
+
51
+ ## batch_update updates object fields in memory, confirm fields changed
52
+ [@customer.name, @customer.email]
53
+ #=> ["Jane Smith", "jane@example.com"]
54
+
55
+ ## batch_update persists to Redis
56
+ @customer.refresh!
57
+ [@customer.name, @customer.email]
58
+ #=> ["Jane Smith", "jane@example.com"]
59
+
60
+ ## batch_update with update_expiration: false works
61
+ @customer.batch_update(name: 'Bob Jones', update_expiration: false)
62
+ @customer.refresh!
63
+ @customer.name
64
+ #=> "Bob Jones"
65
+
66
+ ## apply_fields updates object in memory only (1 of 2)
67
+ @customer.apply_fields(name: 'Memory Only', email: 'memory@test.com')
68
+ [@customer.name, @customer.email]
69
+ #=> ["Memory Only", "memory@test.com"]
70
+
71
+ ## apply_fields doesn't persist to Redis (2 of 2)
72
+ @customer.refresh!
73
+ [@customer.name, @customer.email]
74
+ #=> ["Bob Jones", "jane@example.com"]
75
+
76
+ ## serialize_value handles strings
77
+ @customer.serialize_value("test string")
78
+ #=> "test string"
79
+
80
+ ## serialize_value handles numbers
81
+ @customer.serialize_value(42)
82
+ #=> "42"
83
+
84
+ ## serialize_value handles hashes as JSON
85
+ @customer.serialize_value({key: 'value', num: 123})
86
+ #=> "{\"key\":\"value\",\"num\":123}"
87
+
88
+ ## serialize_value handles arrays as JSON
89
+ @customer.serialize_value([1, 2, 'three'])
90
+ #=> "[1,2,\"three\"]"
91
+
92
+ ## deserialize_value handles JSON strings back to objects
93
+ @customer.deserialize_value('{"key":"value","num":123}')
94
+ #=> {:key=>"value", :num=>123}
95
+
96
+ ## deserialize_value handles JSON arrays
97
+ @customer.deserialize_value('[1,2,"three"]')
98
+ #=> [1, 2, "three"]
99
+
100
+ ## deserialize_value handles plain strings
101
+ @customer.deserialize_value('plain string')
102
+ #=> "plain string"
103
+
104
+ ## transaction method works with block
105
+ result = @customer.transaction do |conn|
106
+ conn.hset @customer.rediskey, 'temp_field', 'temp_value'
107
+ conn.hset @customer.rediskey, 'another_field', 'another_value'
108
+ end
109
+ result.size
110
+ #=> 2
111
+
112
+ ## refresh! reloads from Redis
113
+ @customer.refresh!
114
+ @customer.hget('temp_field')
115
+ #=> "temp_value"
116
+
117
+ ## Empty batch_update still works
118
+ result = @customer.batch_update()
119
+ result.successful?
120
+ #=> true
121
+
122
+ ## destroy! removes object from Redis (1 of 2)
123
+ @customer.destroy!
124
+ #=> true
125
+
126
+ ## After destroy!, Redis key no longer exists (2 of 2)
127
+ @customer.exists?
128
+ #=> false
129
+
130
+ ## destroy! removes object from Redis, not the in-memory object (2 of 2)
131
+ @customer.refresh!
132
+ @customer.name
133
+ #=> "Bob Jones"
134
+
135
+ ## clear_fields! removes the in-memory object fields
136
+ @customer.clear_fields!
137
+ @customer.name
138
+ #=> nil
139
+
140
+ ## Fresh customer for testing new field creation
141
+ @fresh_customer = Customer.new 'fresh-customer@test.com'
142
+ @fresh_customer.class
143
+ #=> Customer
144
+
145
+ ## batch_update with new fields returns [1, 1] for new field creation
146
+ @fresh_customer.remove_field('role')
147
+ @fresh_customer.remove_field('planid')
148
+ @fresh_result = @fresh_customer.batch_update(role: 'admin', planid: 'premium')
149
+ @fresh_result.to_h
150
+ #=> {:success=>true, :results=>[1, 1]}
151
+
152
+ ## Fresh customer fields are set correctly
153
+ [@fresh_customer.role, @fresh_customer.planid]
154
+ #=> ["admin", "premium"]
155
+
156
+ ## Fresh customer changes persist to Redis
157
+ @fresh_customer.refresh!
158
+ [@fresh_customer.role, @fresh_customer.planid]
159
+ #=> ["admin", "premium"]