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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +198 -48
- data/VERSION.yml +1 -1
- data/lib/familia/base.rb +29 -1
- data/lib/familia/features/expiration.rb +90 -0
- data/lib/familia/features/quantization.rb +56 -0
- data/lib/familia/features/safe_dump.rb +2 -33
- data/lib/familia/features.rb +5 -4
- data/lib/familia/horreum/class_methods.rb +172 -53
- data/lib/familia/horreum/commands.rb +43 -5
- data/lib/familia/horreum/relations_management.rb +2 -2
- data/lib/familia/horreum/serialization.rb +172 -47
- data/lib/familia/horreum/settings.rb +0 -8
- data/lib/familia/horreum/utils.rb +0 -1
- data/lib/familia/horreum.rb +1 -1
- data/lib/familia/logging.rb +26 -4
- data/lib/familia/redistype/serialization.rb +60 -38
- data/lib/familia/redistype.rb +45 -17
- data/lib/familia/settings.rb +11 -1
- data/lib/familia/tools.rb +68 -0
- data/lib/familia/types/hashkey.rb +9 -8
- data/lib/familia/types/list.rb +4 -2
- data/lib/familia/types/sorted_set.rb +12 -12
- data/lib/familia/types/string.rb +1 -1
- data/lib/familia/types/unsorted_set.rb +2 -2
- data/lib/familia/utils.rb +106 -51
- data/lib/familia/version.rb +2 -2
- data/try/10_familia_try.rb +4 -4
- data/try/20_redis_type_try.rb +9 -6
- data/try/26_redis_bool_try.rb +1 -1
- data/try/27_redis_horreum_try.rb +1 -1
- data/try/30_familia_object_try.rb +3 -2
- data/try/40_customer_try.rb +3 -3
- data/try/test_helpers.rb +9 -2
- metadata +5 -5
- data/lib/familia/features/api_version.rb +0 -19
- data/lib/familia/features/atomic_saves.rb +0 -8
- data/lib/familia/features/quantizer.rb +0 -35
@@ -1,30 +1,75 @@
|
|
1
1
|
# rubocop:disable all
|
2
2
|
#
|
3
3
|
module Familia
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
# instance-level functionality for Redis operations and object management.
|
4
|
+
|
5
|
+
|
6
|
+
# Familia::Horreum
|
8
7
|
#
|
9
8
|
class Horreum
|
10
|
-
|
11
|
-
#
|
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
|
-
#
|
42
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
57
|
-
ret.all? { |value|
|
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
|
-
#
|
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
|
@@ -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
|
data/lib/familia/horreum.rb
CHANGED
@@ -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 :
|
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
|
|
data/lib/familia/logging.rb
CHANGED
@@ -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 [
|
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
|
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
|
-
|
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
|
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
|
15
|
-
#
|
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
|
18
|
-
#
|
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: "
|
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
|
26
|
-
# to_redis(123) #=> "123"
|
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
|
-
|
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
|
-
|
40
|
-
Familia.ld " from opts[class] <#{opts[:class]}>: #{
|
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
|
39
|
+
if prepared.nil?
|
44
40
|
# Enforce strict values when no class option is specified
|
45
|
-
|
46
|
-
Familia.ld " from
|
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]}> => #{
|
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
|
52
|
-
|
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
|
57
|
-
# can unintentionally return nil if no changes are made, which is
|
58
|
-
# Instead, use compact to ensure the method returns the
|
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
|
data/lib/familia/redistype.rb
CHANGED
@@ -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
|
-
|
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 :
|
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
|
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
|
data/lib/familia/settings.rb
CHANGED
@@ -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
|