familia 1.0.0.pre.rc2 → 1.0.0.pre.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +198 -48
  4. data/VERSION.yml +1 -1
  5. data/lib/familia/base.rb +29 -1
  6. data/lib/familia/features/expiration.rb +90 -0
  7. data/lib/familia/features/quantization.rb +56 -0
  8. data/lib/familia/features/safe_dump.rb +2 -33
  9. data/lib/familia/features.rb +5 -4
  10. data/lib/familia/horreum/class_methods.rb +172 -53
  11. data/lib/familia/horreum/commands.rb +43 -5
  12. data/lib/familia/horreum/relations_management.rb +2 -2
  13. data/lib/familia/horreum/serialization.rb +172 -47
  14. data/lib/familia/horreum/settings.rb +0 -8
  15. data/lib/familia/horreum/utils.rb +0 -1
  16. data/lib/familia/horreum.rb +1 -1
  17. data/lib/familia/logging.rb +26 -4
  18. data/lib/familia/redistype/serialization.rb +60 -38
  19. data/lib/familia/redistype.rb +45 -17
  20. data/lib/familia/settings.rb +11 -1
  21. data/lib/familia/tools.rb +68 -0
  22. data/lib/familia/types/hashkey.rb +9 -8
  23. data/lib/familia/types/list.rb +4 -2
  24. data/lib/familia/types/sorted_set.rb +12 -12
  25. data/lib/familia/types/string.rb +1 -1
  26. data/lib/familia/types/unsorted_set.rb +2 -2
  27. data/lib/familia/utils.rb +106 -51
  28. data/lib/familia/version.rb +2 -2
  29. data/try/10_familia_try.rb +4 -4
  30. data/try/20_redis_type_try.rb +9 -6
  31. data/try/26_redis_bool_try.rb +1 -1
  32. data/try/27_redis_horreum_try.rb +1 -1
  33. data/try/30_familia_object_try.rb +3 -2
  34. data/try/40_customer_try.rb +3 -3
  35. data/try/test_helpers.rb +9 -2
  36. metadata +5 -5
  37. data/lib/familia/features/api_version.rb +0 -19
  38. data/lib/familia/features/atomic_saves.rb +0 -8
  39. data/lib/familia/features/quantizer.rb +0 -35
@@ -1,30 +1,75 @@
1
1
  # rubocop:disable all
2
2
  #
3
3
  module Familia
4
- # InstanceMethods - Module containing instance-level methods for Familia
5
- #
6
- # This module is included in classes that include Familia, providing
7
- # instance-level functionality for Redis operations and object management.
4
+
5
+
6
+ # Familia::Horreum
8
7
  #
9
8
  class Horreum
10
-
11
- # Methods that call load and dump (InstanceMethods)
9
+ # Serialization: Where Objects Go to Become Strings (and Vice Versa)!
10
+ #
11
+ # This module is chock-full of methods that'll make your head spin (in a
12
+ # good way)! We've got loaders, dumpers, and refreshers galore. It's like
13
+ # a laundromat for your data, but instead of quarters, it runs on Redis commands.
14
+ #
15
+ # A Note on Our Refreshing Refreshers:
16
+ # In the wild world of Ruby, '!' usually means "Watch out! I'm dangerous!"
17
+ # But here in Familia-land, we march to the beat of a different drummer.
18
+ # Our refresh! method is the real deal, doing all the heavy lifting.
19
+ # The non-bang refresh? Oh, it's just as rowdy, but it plays nice with
20
+ # method chaining. It's like the polite twin who still knows how to party.
21
+ #
22
+ # Remember: In Familia, refreshing isn't just a chore, it's a chance to
23
+ # dance with data! Whether you bang(!) or not, you're still invited to
24
+ # the Redis disco.
25
+ #
26
+ # (P.S. If you're reading these docs, lol sorry. I asked Claude 3.5 to
27
+ # write in the style of _why the lucky stiff today and got this uncanny
28
+ # valley response. I hope you enjoy reading it as much as I did writing
29
+ # the prompt for it. - @delano).
30
+ #
31
+ # (Ahem! What I meant to say was that if you're reading this, congratulations!
32
+ # You've stumbled upon the secret garden of documentation. Feel free to smell
33
+ # the Ruby roses, but watch out for the Redis thorns!)
12
34
  #
13
- # Note on refresh methods:
14
- # In this class, refresh! is the primary method that performs the Redis
15
- # query and state update. The non-bang refresh method is provided as a
16
- # convenience for method chaining, but still performs the same destructive
17
- # update as refresh!. This deviates from common Ruby conventions to better
18
- # fit the specific needs of this system.
19
35
  module Serialization
20
- #include Familia::RedisType::Serialization
21
36
 
22
37
  attr_writer :redis
23
38
 
39
+ # Summon the mystical Redis connection from the depths of instance or class.
40
+ #
41
+ # This method is like a magical divining rod, always pointing to the nearest
42
+ # source of Redis goodness. It first checks if we have a personal Redis
43
+ # connection (@redis), and if not, it borrows the class's connection.
44
+ #
45
+ # @return [Redis] A shimmering Redis connection, ready for your bidding.
46
+ #
47
+ # @example Finding your Redis way
48
+ # puts object.redis
49
+ # # => #<Redis client v4.5.1 for redis://localhost:6379/0>
50
+ #
24
51
  def redis
25
52
  @redis || self.class.redis
26
53
  end
27
54
 
55
+ # Perform a sacred Redis transaction ritual.
56
+ #
57
+ # This method creates a protective circle around your Redis operations,
58
+ # ensuring they all succeed or fail together. It's like a group hug for your
59
+ # data operations, but with more ACID properties.
60
+ #
61
+ # @yield [conn] A block where you can perform your Redis incantations.
62
+ # @yieldparam conn [Redis] A Redis connection in multi mode.
63
+ #
64
+ # @example Performing a Redis rain dance
65
+ # transaction do |conn|
66
+ # conn.set("weather", "rainy")
67
+ # conn.set("mood", "melancholic")
68
+ # end
69
+ #
70
+ # @note This method temporarily replaces your Redis connection with a multi
71
+ # connection. Don't worry, it puts everything back where it found it when it's done.
72
+ #
28
73
  def transaction
29
74
  original_redis = self.redis
30
75
 
@@ -38,34 +83,111 @@ module Familia
38
83
  end
39
84
  end
40
85
 
41
- # A thin wrapper around `commit_fields` that updates the timestamps and
42
- # returns a boolean.
86
+ # Save our precious data to Redis, with a sprinkle of timestamp magic!
87
+ #
88
+ # This method is like a conscientious historian, not only recording your
89
+ # object's current state but also meticulously timestamping when it was
90
+ # created and last updated. It's the record keeper of your data's life story!
91
+ #
92
+ # @return [Boolean] true if the save was successful, false if Redis was grumpy.
93
+ #
94
+ # @example Preserving your pet rock for posterity
95
+ # rocky = PetRock.new(name: "Dwayne")
96
+ # rocky.save
97
+ # # => true (Dwayne is now immortalized in Redis)
98
+ #
99
+ # @note This method will leave breadcrumbs (traces) if you're in debug mode.
100
+ # It's like Hansel and Gretel, but for data operations!
101
+ #
43
102
  def save
44
103
  Familia.trace :SAVE, redis, redisuri, caller(1..1) if Familia.debug?
45
104
 
46
- # Update timestamp fields
105
+ # Update our object's life story
47
106
  self.key ||= self.identifier
48
107
  self.updated = Familia.now.to_i
49
108
  self.created ||= Familia.now.to_i
50
109
 
51
- # Thr return value of commit_fields is an array of strings: ["OK"].
110
+ # Commit our tale to the Redis chronicles
52
111
  ret = commit_fields # e.g. ["OK"]
53
112
 
54
113
  Familia.ld "[save] #{self.class} #{rediskey} #{ret}"
55
114
 
56
- # Convert the return value to a boolean
57
- ret.all? { |value| value == "OK" }
115
+ # Did Redis accept our offering?
116
+ ret.uniq.all? { |value| ["OK", true].include?(value) }
117
+ end
118
+
119
+ # Apply a smattering of fields to this object like fairy dust.
120
+ #
121
+ # @param fields [Hash] A magical bag of named attributes to sprinkle onto
122
+ # this instance. Each key-value pair is like a tiny spell, ready to
123
+ # enchant our object's properties.
124
+ #
125
+ # @return [self] Returns the newly bejeweled instance, now sparkling with
126
+ # fresh attributes.
127
+ #
128
+ # @example Giving your object a makeover
129
+ # dragon.apply_fields(name: "Puff", breathes: "fire", loves: "little boys
130
+ # named Jackie")
131
+ # # => #<Dragon:0x007f8a1c8b0a28 @name="Puff", @breathes="fire",
132
+ # @loves="little boys named Jackie">
133
+ #
134
+ def apply_fields(**fields)
135
+ fields.each do |field, value|
136
+ # Whisper the new value into the object's ear (if it's listening)
137
+ send("#{field}=", value) if respond_to?("#{field}=")
138
+ end
139
+ self
58
140
  end
59
141
 
60
- # +return: [Array<String>] The return value of the Redis multi command
142
+ # Commit our precious fields to Redis.
143
+ #
144
+ # This method performs a sacred ritual, sending our cherished attributes
145
+ # on a journey through the ethernet to find their resting place in Redis.
146
+ #
147
+ # @return [Array<String>] A mystical array of strings, cryptic messages
148
+ # from the Redis gods.
149
+ #
150
+ # @note Be warned, young programmer! This method dabbles in the arcane
151
+ # art of transactions. Side effects may include data persistence and a
152
+ # slight tingling sensation.
153
+ #
154
+ # @example Offering your changes to the Redis deities
155
+ # unicorn.name = "Charlie"
156
+ # unicorn.horn_length = "magnificent"
157
+ # unicorn.commit_fields
158
+ # # => ["OK", "OK"] (The Redis gods are pleased with your offering)
159
+ #
61
160
  def commit_fields
62
161
  Familia.ld "[commit_fields] #{self.class} #{rediskey} #{to_h}"
63
162
  transaction do |conn|
64
163
  hmset
164
+
165
+ # Only classes that have the expiration ferature enabled will
166
+ # actually set an expiration time on their keys. Otherwise
167
+ # this will be a no-op.
65
168
  update_expiration
66
169
  end
67
170
  end
68
171
 
172
+ # Dramatically vanquish this object from the face of Redis! (ed: delete it)
173
+ #
174
+ # This method is the doomsday device of our little data world. It will
175
+ # mercilessly eradicate all traces of our object from Redis, leaving naught
176
+ # but digital dust in its wake. Use with caution, lest you accidentally
177
+ # destroy the wrong data-verse!
178
+ #
179
+ # @return [void] Returns nothing, for nothing remains after destruction.
180
+ #
181
+ # @example Bidding a fond farewell to your pet rock
182
+ # rocky = PetRock.new(name: "Dwayne")
183
+ # rocky.destroy!
184
+ # # => *poof* Rocky is no more. A moment of silence, please.
185
+ #
186
+ # @note If debugging is enabled, this method will leave a trace of its
187
+ # destructive path, like breadcrumbs for future data archaeologists.
188
+ #
189
+ # @see #delete! The actual hitman carrying out the deed.
190
+ #
69
191
  def destroy!
70
192
  Familia.trace :DESTROY, redis, redisuri, caller(1..1) if Familia.debug?
71
193
  delete!
@@ -98,9 +220,22 @@ module Familia
98
220
  self
99
221
  end
100
222
 
223
+ # Transform this object into a magical hash of wonders!
224
+ #
225
+ # This method performs an alchemical transmutation, turning our noble object
226
+ # into a more plebeian hash. But fear not, for in this form, it can slip through
227
+ # the cracks of the universe (or at least, into Redis) with ease.
228
+ #
229
+ # @return [Hash] A glittering hash, each key a field name, each value a
230
+ # Redis-ready treasure.
231
+ #
232
+ # @example Turning your dragon into a hash
233
+ # dragon.to_h
234
+ # # => {"name"=>"Puff", "breathes"=>"fire", "age"=>1000}
235
+ #
236
+ # @note Watch in awe as each field is lovingly prepared for its Redis adventure!
237
+ #
101
238
  def to_h
102
- # Use self.class.fields to efficiently generate a hash
103
- # of all the fields for this object
104
239
  self.class.fields.inject({}) do |hsh, field|
105
240
  val = send(field)
106
241
  prepared = to_redis(val)
@@ -110,6 +245,21 @@ module Familia
110
245
  end
111
246
  end
112
247
 
248
+ # Line up all our attributes in a neat little array parade!
249
+ #
250
+ # This method marshals all our object's attributes into an orderly procession,
251
+ # ready to march into Redis in perfect formation. It's like a little data army,
252
+ # but friendlier and less prone to conquering neighboring databases.
253
+ #
254
+ # @return [Array] A splendid array of Redis-ready values, in the order of our fields.
255
+ #
256
+ # @example Arranging your unicorn's attributes in a line
257
+ # unicorn.to_a
258
+ # # => ["Charlie", "magnificent", 5]
259
+ #
260
+ # @note Each value is carefully disguised in its Redis costume
261
+ # before joining the parade.
262
+ #
113
263
  def to_a
114
264
  self.class.fields.map do |field|
115
265
  val = send(field)
@@ -160,34 +310,9 @@ module Familia
160
310
  prepared
161
311
  end
162
312
 
163
- def update_expiration(ttl = nil)
164
- ttl ||= opts[:ttl]
165
- return if ttl.to_i.zero? # nil will be zero
166
-
167
- Familia.ld "#{rediskey} to #{ttl}"
168
- expire ttl.to_i
169
- end
170
313
  end
171
314
  # End of Serialization module
172
315
 
173
316
  include Serialization # these become Horreum instance methods
174
317
  end
175
318
  end
176
-
177
- __END__
178
-
179
- # Consider adding a retry mechanism for the refresh operation
180
- # if it fails to fetch the expected data:
181
- def refresh_with_retry(max_attempts = 3)
182
- attempts = 0
183
- base = 2
184
- while attempts < max_attempts
185
- refresh!
186
- return if name == "Jane Doe" # Or whatever condition indicates a successful refresh
187
- attempts += 1
188
-
189
- sleep_time = 0.1 * (base ** attempts)
190
- sleep(sleep_time) # Exponential backoff
191
- end
192
- raise "Failed to refresh after #{max_attempts} attempts"
193
- end
@@ -29,14 +29,6 @@ module Familia
29
29
  }
30
30
  end
31
31
 
32
- def ttl=(v)
33
- @ttl = v.to_i
34
- end
35
-
36
- def ttl
37
- @ttl || self.class.ttl
38
- end
39
-
40
32
  def db=(v)
41
33
  @db = v.to_i
42
34
  end
@@ -28,7 +28,6 @@ module Familia
28
28
  # from the `identifier` method).
29
29
  #
30
30
  def rediskey(suffix = nil, ignored = nil)
31
- Familia.ld "[#rediskey] #{identifier} for #{self.class}"
32
31
  raise Familia::NoIdentifier, "No identifier for #{self.class}" if identifier.to_s.empty?
33
32
  suffix ||= self.suffix # use the instance method to get the default suffix
34
33
  self.class.rediskey identifier, suffix
@@ -56,7 +56,7 @@ module Familia
56
56
  class << self
57
57
  # Extends ClassMethods to subclasses and tracks Familia members
58
58
  def inherited(member)
59
- Familia.trace :INHERITED, nil, "Inherited by #{member}", caller if Familia.debug?
59
+ Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
60
60
  member.extend(ClassMethods)
61
61
  member.extend(Features)
62
62
 
@@ -126,26 +126,48 @@ module Familia
126
126
  #
127
127
  # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
128
128
  # :FROMREDIS, :LOAD, :EXISTS).
129
- # @param redis_instance [Object] The Redis instance being used.
129
+ # @param redis_instance [Redis, Redis::Future, nil] The Redis instance or
130
+ # Future being used.
130
131
  # @param ident [String] An identifier or key related to the operation being
131
132
  # traced.
132
133
  # @param context [Array<String>, String, nil] The calling context, typically
133
134
  # obtained from `caller` or `caller.first`. Default is nil.
134
135
  #
135
136
  # @example
136
- # Familia.trace :LOAD, Familia.redis(uri), objkey, caller if Familia.debug?
137
- #
137
+ # Familia.trace :LOAD, Familia.redis(uri), objkey, caller(1..1) if
138
+ # Familia.debug?
138
139
  #
139
140
  # @return [nil]
140
141
  #
142
+ # @note This method only executes if LoggerTraceRefinement::ENABLED is true.
143
+ # @note The redis_instance can be a Redis object, Redis::Future (used in
144
+ # pipelined and multi blocks), or nil (when the redis connection isn't
145
+ # relevant).
146
+ #
141
147
  def trace(label, redis_instance, ident, context = nil)
142
148
  return unless LoggerTraceRefinement::ENABLED
143
- instance_id = redis_instance&.id
149
+
150
+ # Usually redis_instance is a Redis object, but it could be
151
+ # a Redis::Future which is what is used inside of pipelined
152
+ # and multi blocks. In some contexts it's nil where the
153
+ # redis connection isn't relevant.
154
+ instance_id = if redis_instance
155
+ case redis_instance
156
+ when Redis
157
+ redis_instance.id.respond_to?(:to_s) ? redis_instance.id.to_s : redis_instance.class.name
158
+ when Redis::Future
159
+ "Redis::Future"
160
+ else
161
+ redis_instance.class.name
162
+ end
163
+ end
164
+
144
165
  codeline = if context
145
166
  context = [context].flatten
146
167
  context.reject! { |line| line =~ %r{lib/familia} }
147
168
  context.first
148
169
  end
170
+
149
171
  @logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
150
172
  end
151
173
 
@@ -4,63 +4,80 @@ class Familia::RedisType
4
4
 
5
5
  module Serialization
6
6
 
7
- # Serializes an individual value for storage in Redis.
8
- #
9
- # This method prepares a value for storage in Redis by converting it to a string representation.
10
- # If a class option is specified, it uses that class's serialization method.
11
- # Otherwise, it relies on the value's own `to_s` method for serialization.
7
+ # Serializes a value for storage in Redis.
12
8
  #
13
9
  # @param val [Object] The value to be serialized.
14
- # @param strict_values [Boolean] Whether to enforce strict value serialization (default: true). Only applies when no class option is specified because the class option is assumed to handle its own serialization.
15
- # @return [String] The serialized representation of the value.
10
+ # @param strict_values [Boolean] Whether to enforce strict value
11
+ # serialization (default: true).
12
+ # @return [String, nil] The serialized representation of the value, or nil
13
+ # if serialization fails.
16
14
  #
17
- # @note When no class option is specified, this method attempts to serialize the value directly.
18
- # If the serialization fails, it falls back to the value's own string representation.
15
+ # @note When a class option is specified, it uses that class's
16
+ # serialization method. Otherwise, it relies on Familia.distinguisher for
17
+ # serialization.
19
18
  #
20
19
  # @example With a class option
21
- # to_redis(User.new(name: "John"), strict_values: false) #=> '{"name":"John"}'
22
- # to_redis(nil, strict_values: false) #=> "" (empty string)
23
- # to_redis(true, strict_values: false) #=> "true"
20
+ # to_redis(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
24
21
  #
25
- # @example Without a class option and strict values
26
- # to_redis(123) #=> "123" (which becomes "123" in Redis)
22
+ # @example Without a class option
23
+ # to_redis(123) #=> "123"
27
24
  # to_redis("hello") #=> "hello"
28
- # to_redis(nil) # raises an exception
29
- # to_redis(true) # raises an exception
30
25
  #
31
- # @raise [Familia::HighRiskFactor]
26
+ # @raise [Familia::HighRiskFactor] If serialization fails under strict
27
+ # mode.
32
28
  #
33
29
  def to_redis(val, strict_values = true)
34
- ret = nil
30
+ prepared = nil
35
31
 
36
32
  Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}>", caller(1..1) if Familia.debug?
37
33
 
38
34
  if opts[:class]
39
- ret = Familia.distinguisher(opts[:class], strict_values)
40
- Familia.ld " from opts[class] <#{opts[:class]}>: #{ret||'<nil>'}"
35
+ prepared = Familia.distinguisher(opts[:class], strict_values)
36
+ Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared||'<nil>'}"
41
37
  end
42
38
 
43
- if ret.nil?
39
+ if prepared.nil?
44
40
  # Enforce strict values when no class option is specified
45
- ret = Familia.distinguisher(val, true)
46
- Familia.ld " from value #{val}<#{val.class}>: #{ret}<#{ret.class}>"
41
+ prepared = Familia.distinguisher(val, true)
42
+ Familia.ld " from <#{val.class}> => <#{prepared.class}>"
47
43
  end
48
44
 
49
- Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}> => #{ret}<#{ret.class}>", caller(1..1) if Familia.debug?
45
+ Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>", caller(1..1) if Familia.debug?
50
46
 
51
- Familia.warn "[#{self.class}\#to_redis] nil returned for #{opts[:class]}\##{name}" if ret.nil?
52
- ret
47
+ Familia.warn "[#{self.class}\#to_redis] nil returned for #{opts[:class]}\##{name}" if prepared.nil?
48
+ prepared
53
49
  end
54
50
 
51
+ # Deserializes multiple values from Redis, removing nil values.
52
+ #
53
+ # @param values [Array<String>] The values to deserialize.
54
+ # @return [Array<Object>] Deserialized objects, with nil values removed.
55
+ #
56
+ # @see #multi_from_redis_with_nil
57
+ #
55
58
  def multi_from_redis(*values)
56
- # Avoid using compact! here. Using compact! as the last expression in the method
57
- # can unintentionally return nil if no changes are made, which is not desirable.
58
- # Instead, use compact to ensure the method returns the expected value.
59
+ # Avoid using compact! here. Using compact! as the last expression in the
60
+ # method can unintentionally return nil if no changes are made, which is
61
+ # not desirable. Instead, use compact to ensure the method returns the
62
+ # expected value.
59
63
  multi_from_redis_with_nil(*values).compact
60
64
  end
61
65
 
66
+ # Deserializes multiple values from Redis, preserving nil values.
67
+ #
68
+ # @param values [Array<String>] The values to deserialize.
69
+ # @return [Array<Object, nil>] Deserialized objects, including nil values.
70
+ #
71
+ # @raise [Familia::Problem] If the specified class doesn't respond to the
72
+ # load method.
73
+ #
74
+ # @note This method attempts to deserialize each value using the specified
75
+ # class's load method. If deserialization fails for a value, it's
76
+ # replaced with nil.
77
+ #
62
78
  # NOTE: `multi` in this method name refers to multiple values from
63
79
  # redis and not the Redis server MULTI command.
80
+ #
64
81
  def multi_from_redis_with_nil(*values)
65
82
  Familia.ld "multi_from_redis: (#{@opts}) #{values}"
66
83
  return [] if values.empty?
@@ -89,6 +106,20 @@ class Familia::RedisType
89
106
  values
90
107
  end
91
108
 
109
+ # Deserializes a single value from Redis.
110
+ #
111
+ # @param val [String, nil] The value to deserialize.
112
+ # @return [Object, nil] The deserialized object, the default value if
113
+ # val is nil, or nil if deserialization fails.
114
+ #
115
+ # @note If no class option is specified, the original value is
116
+ # returned unchanged.
117
+ #
118
+ # NOTE: Currently only the RedisType class uses this method. Horreum
119
+ # fields are a newer addition and don't support the full range of
120
+ # deserialization options that RedisType supports. It uses to_redis
121
+ # for serialization since everything becomes a string in Redis.
122
+ #
92
123
  def from_redis(val)
93
124
  return @opts[:default] if val.nil?
94
125
  return val unless @opts[:class]
@@ -96,15 +127,6 @@ class Familia::RedisType
96
127
  ret = multi_from_redis val
97
128
  ret&.first # return the object or nil
98
129
  end
99
-
100
- def update_expiration(ttl = nil)
101
- ttl ||= opts[:ttl]
102
- return if ttl.to_i.zero? # nil will be zero
103
-
104
- Familia.ld "#{rediskey} to #{ttl}"
105
- expire ttl.to_i
106
- end
107
-
108
130
  end
109
131
 
110
132
  end
@@ -13,31 +13,29 @@ module Familia
13
13
  # @abstract Subclass and implement Redis data type specific methods
14
14
  class RedisType
15
15
  include Familia::Base
16
+ extend Familia::Features
16
17
 
17
18
  @registered_types = {}
18
19
  @valid_options = %i[class parent ttl default db key redis]
19
20
  @db = nil
20
- @ttl = nil
21
+
22
+ feature :expiration
23
+ feature :quantization
21
24
 
22
25
  class << self
23
26
  attr_reader :registered_types, :valid_options
24
27
  attr_accessor :parent
25
- attr_writer :ttl, :db, :uri
28
+ attr_writer :db, :uri
26
29
 
27
30
  # To be called inside every class that inherits RedisType
28
31
  # +methname+ is the term used for the class and instance methods
29
32
  # that are created for the given +klass+ (e.g. set, list, etc)
30
33
  def register(klass, methname)
31
- Familia.ld "[#{self}] Registering #{klass} as #{methname}"
34
+ Familia.ld "[#{self}] Registering #{klass} as #{methname.inspect}"
32
35
 
33
36
  @registered_types[methname] = klass
34
37
  end
35
38
 
36
- def ttl(val = nil)
37
- @ttl = val unless val.nil?
38
- @ttl || parent&.ttl
39
- end
40
-
41
39
  def db(val = nil)
42
40
  @db = val unless val.nil?
43
41
  @db || parent&.db
@@ -49,8 +47,9 @@ module Familia
49
47
  end
50
48
 
51
49
  def inherited(obj)
50
+ Familia.trace :REDISTYPE, nil, "#{obj} is my kinda type", caller(1..1) if Familia.debug?
52
51
  obj.db = db
53
- obj.ttl = ttl
52
+ obj.ttl = ttl # method added via Features::Expiration
54
53
  obj.uri = uri
55
54
  obj.parent = self
56
55
  super(obj)
@@ -101,6 +100,12 @@ module Familia
101
100
  @opts = opts || {}
102
101
  @opts = RedisType.valid_keys_only(@opts)
103
102
 
103
+ # Apply the options to instance method setters of the same name
104
+ @opts.each do |k, v|
105
+ Familia.ld " [setting] #{k} #{v}"
106
+ send(:"#{k}=", v) if respond_to? :"#{k}="
107
+ end
108
+
104
109
  init if respond_to? :init
105
110
  end
106
111
 
@@ -110,10 +115,37 @@ module Familia
110
115
  parent? ? parent.redis : Familia.redis(opts[:db])
111
116
  end
112
117
 
113
- # Produces the full redis key for this object.
118
+ # Produces the full Redis key for this object.
119
+ #
120
+ # @return [String] The full Redis key.
121
+ #
122
+ # This method determines the appropriate Redis key based on the context of the RedisType object:
123
+ #
124
+ # 1. If a hardcoded key is set in the options, it returns that key.
125
+ # 2. For instance-level RedisType objects, it uses the parent instance's rediskey method.
126
+ # 3. For class-level RedisType objects, it uses the parent class's rediskey method.
127
+ # 4. For standalone RedisType objects, it uses the keystring as the full Redis key.
128
+ #
129
+ # For class-level RedisType objects (parent_class? == true):
130
+ # - The suffix is optional and used to differentiate between different types of objects.
131
+ # - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
132
+ # - If a nil suffix is explicitly passed, it won't appear in the resulting Redis key.
133
+ # - Passing nil as the suffix is how class-level RedisType objects are created without
134
+ # the global default 'object' suffix.
135
+ #
136
+ # @example Instance-level RedisType
137
+ # user_instance.some_redistype.rediskey # => "user:123:some_redistype"
138
+ #
139
+ # @example Class-level RedisType
140
+ # User.some_redistype.rediskey # => "user:some_redistype"
141
+ #
142
+ # @example Standalone RedisType
143
+ # RedisType.new("mykey").rediskey # => "mykey"
144
+ #
145
+ # @example Class-level RedisType with explicit nil suffix
146
+ # User.rediskey("123", nil) # => "user:123"
147
+ #
114
148
  def rediskey
115
- Familia.ld "[rediskey] #{keystring} for #{self.class} (#{opts})"
116
-
117
149
  # Return the hardcoded key if it's set. This is useful for
118
150
  # support legacy keys that aren't derived in the same way.
119
151
  return opts[:key] if opts[:key]
@@ -128,7 +160,7 @@ module Familia
128
160
  parent.rediskey(keystring, nil)
129
161
  else
130
162
  # This is a standalone RedisType object where it's keystring
131
- # is the full key.
163
+ # is the full redis key.
132
164
  keystring
133
165
  end
134
166
  end
@@ -153,10 +185,6 @@ module Familia
153
185
  @opts[:parent]
154
186
  end
155
187
 
156
- def ttl
157
- @opts[:ttl] || self.class.ttl
158
- end
159
-
160
188
  def db
161
189
  @opts[:db] || self.class.db
162
190
  end
@@ -5,7 +5,7 @@ module Familia
5
5
  @delim = ':'
6
6
  @prefix = nil
7
7
  @suffix = :object
8
- @ttl = nil
8
+ @ttl = 0 # see update_expiration. Zero is skip. nil is an exception.
9
9
  @db = nil
10
10
 
11
11
  module Settings
@@ -27,6 +27,16 @@ module Familia
27
27
  @suffix
28
28
  end
29
29
 
30
+ def ttl(v = nil)
31
+ @ttl = v unless v.nil?
32
+ @ttl
33
+ end
34
+
35
+ def db(v = nil)
36
+ @db = v unless v.nil?
37
+ @db
38
+ end
39
+
30
40
  # We define this do-nothing method because it reads better
31
41
  # than simply Familia.suffix in some contexts.
32
42
  def default_suffix