familia 1.0.0.pre.rc4 → 1.0.0.pre.rc6
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 +1 -1
- data/VERSION.yml +1 -1
- data/lib/familia/base.rb +1 -1
- data/lib/familia/features/expiration.rb +19 -18
- data/lib/familia/horreum/class_methods.rb +10 -6
- data/lib/familia/horreum/commands.rb +3 -2
- data/lib/familia/horreum/serialization.rb +218 -55
- data/lib/familia/horreum/utils.rb +1 -1
- data/lib/familia/horreum.rb +14 -11
- data/lib/familia/redistype/serialization.rb +3 -3
- data/lib/familia/redistype.rb +6 -1
- data/lib/familia/types/sorted_set.rb +4 -4
- data/lib/familia/utils.rb +47 -22
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d368f329560a1afd7252a2c8de830a7230334933cd00715b4b952e44d9fbf63
|
4
|
+
data.tar.gz: edd7b843ff07cf87aa2fe46766d7b0fd6dfa814b5404c9ca04bc42de18b05619
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f4db87dbaa2c12636d8293a477ef1e2a000364d3e6e2515cac90dad7277efc29d3670103cdf4f7697960ec90012ddec7071004146785ec7d3c838863e1a241b
|
7
|
+
data.tar.gz: a1812fce749dc485753a10c39281feb1babab24fd95ecdd553398c4a07a800836d7bf4ac4a7574ee987d4d181842e089cd732efa343141a0bf4610594ea8f1cd
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
data/VERSION.yml
CHANGED
data/lib/familia/base.rb
CHANGED
@@ -43,7 +43,7 @@ module Familia
|
|
43
43
|
#
|
44
44
|
# @note This method is a no-op. It's like shouting into the void, but less echo-y.
|
45
45
|
#
|
46
|
-
def update_expiration(
|
46
|
+
def update_expiration(*)
|
47
47
|
Familia.info "[update_expiration] Skipped for #{rediskey}. #{self.class} data is immortal!"
|
48
48
|
nil
|
49
49
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
|
5
5
|
module Familia::Features
|
6
|
-
|
6
|
+
|
7
7
|
module Expiration
|
8
8
|
@ttl = nil
|
9
9
|
|
@@ -37,27 +37,27 @@ module Familia::Features
|
|
37
37
|
@ttl || self.class.ttl
|
38
38
|
end
|
39
39
|
|
40
|
-
#
|
40
|
+
# Sets an expiration time for the Redis data associated with this object.
|
41
41
|
#
|
42
|
-
#
|
43
|
-
#
|
42
|
+
# This method allows setting a Time To Live (TTL) for the data in Redis,
|
43
|
+
# after which it will be automatically removed.
|
44
44
|
#
|
45
|
-
# @param ttl [Integer, nil] The Time To Live in seconds.
|
46
|
-
#
|
45
|
+
# @param ttl [Integer, nil] The Time To Live in seconds. If nil, the default
|
46
|
+
# TTL will be used.
|
47
47
|
#
|
48
|
-
# @return [Boolean]
|
49
|
-
#
|
48
|
+
# @return [Boolean] Returns true if the expiration was set successfully,
|
49
|
+
# false otherwise.
|
50
50
|
#
|
51
|
-
# @example
|
52
|
-
#
|
51
|
+
# @example Setting an expiration of one day
|
52
|
+
# object.update_expiration(86400)
|
53
53
|
#
|
54
|
-
# @note If TTL is zero,
|
55
|
-
#
|
54
|
+
# @note If TTL is set to zero, the expiration will be removed, making the
|
55
|
+
# data persist indefinitely.
|
56
56
|
#
|
57
|
-
# @raise [Familia::Problem]
|
58
|
-
#
|
57
|
+
# @raise [Familia::Problem] Raises an error if the TTL is not a non-negative
|
58
|
+
# integer.
|
59
59
|
#
|
60
|
-
|
60
|
+
def update_expiration(ttl = nil)
|
61
61
|
ttl ||= self.ttl
|
62
62
|
# It's important to raise exceptions here and not just log warnings. We
|
63
63
|
# don't want to silently fail at setting expirations and cause data
|
@@ -67,14 +67,15 @@ module Familia::Features
|
|
67
67
|
# good reason for the ttl to not be set in the first place. If the
|
68
68
|
# class doesn't have a ttl, the default comes from Familia.ttl (which
|
69
69
|
# is 0).
|
70
|
-
|
71
|
-
|
70
|
+
unless ttl.is_a?(Numeric)
|
71
|
+
raise Familia::Problem, "TTL must be a number (#{ttl.class} in #{self.class})"
|
72
|
+
end
|
72
73
|
|
73
74
|
if ttl.zero?
|
74
75
|
return Familia.ld "[update_expiration] No expiration for #{self.class} (#{rediskey})"
|
75
76
|
end
|
76
77
|
|
77
|
-
Familia.
|
78
|
+
Familia.ld "[update_expiration] Expires #{rediskey} in #{ttl} seconds"
|
78
79
|
|
79
80
|
# Redis' EXPIRE command returns 1 if the timeout was set, 0 if key does
|
80
81
|
# not exist or the timeout could not be set. Via redis-rb here, it's
|
@@ -78,7 +78,11 @@ module Familia
|
|
78
78
|
fast_writer! name
|
79
79
|
end
|
80
80
|
|
81
|
-
# Defines a writer method with a bang (!) suffix for a given
|
81
|
+
# Defines a fast writer method with a bang (!) suffix for a given
|
82
|
+
# attribute name. Fast writer methods are used to immediately persist
|
83
|
+
# attribute values to Redis. Calling a fast writer method has no
|
84
|
+
# effect on any of the object's other attributes and does not trigger
|
85
|
+
# a called to update the object's expiration time.
|
82
86
|
#
|
83
87
|
# The dynamically defined method performs the following:
|
84
88
|
# - Checks if the correct number of arguments is provided (exactly one).
|
@@ -97,18 +101,18 @@ module Familia
|
|
97
101
|
# Check if the correct number of arguments is provided (exactly one).
|
98
102
|
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size != 1
|
99
103
|
|
100
|
-
|
104
|
+
val = args.first
|
101
105
|
|
102
106
|
begin
|
103
107
|
# Trace the operation if debugging is enabled.
|
104
|
-
Familia.trace :FAST_WRITER, redis, "#{name}: #{
|
108
|
+
Familia.trace :FAST_WRITER, redis, "#{name}: #{val.inspect}", caller(1..1) if Familia.debug?
|
105
109
|
|
106
110
|
# Convert the provided value to a format suitable for Redis storage.
|
107
|
-
prepared = to_redis(
|
108
|
-
Familia.ld "[.fast_writer!] #{name} val: #{
|
111
|
+
prepared = to_redis(val)
|
112
|
+
Familia.ld "[.fast_writer!] #{name} val: #{val.class} prepared: #{prepared.class}"
|
109
113
|
|
110
114
|
# Use the existing accessor method to set the attribute value.
|
111
|
-
send :"#{name}=",
|
115
|
+
send :"#{name}=", val
|
112
116
|
|
113
117
|
# Persist the value to Redis immediately using the hset command.
|
114
118
|
hset name, prepared
|
@@ -23,8 +23,9 @@ module Familia
|
|
23
23
|
self.class.exists? identifier, suffix
|
24
24
|
end
|
25
25
|
|
26
|
-
# Sets a timeout on key. After the timeout has expired, the key will
|
27
|
-
# Returns 1 if the timeout was set, 0 if key
|
26
|
+
# Sets a timeout on key. After the timeout has expired, the key will
|
27
|
+
# automatically be deleted. Returns 1 if the timeout was set, 0 if key
|
28
|
+
# does not exist or the timeout could not be set.
|
28
29
|
#
|
29
30
|
def expire(ttl = nil)
|
30
31
|
ttl ||= self.class.ttl
|
@@ -6,6 +6,32 @@ module Familia
|
|
6
6
|
# Familia::Horreum
|
7
7
|
#
|
8
8
|
class Horreum
|
9
|
+
# The Sacred Scrolls of Redis Responses
|
10
|
+
#
|
11
|
+
# Behold! The mystical runes that Redis whispers back to us:
|
12
|
+
#
|
13
|
+
# "OK" - The sweet sound of success, like a tiny "ding!" from the depths of data.
|
14
|
+
# true - The boolean Buddha nods in agreement.
|
15
|
+
# 1 - A lonely digit, standing tall and proud. "I did something!" it proclaims.
|
16
|
+
# 0 - The silent hero. It tried its best, bless its heart.
|
17
|
+
# nil - The zen master of responses. It's not nothing, it's... enlightenment!
|
18
|
+
#
|
19
|
+
# These sacred signs are our guide through the Redis wilderness. When we cast
|
20
|
+
# our spells (er, commands), we seek these friendly faces in the returned
|
21
|
+
# smoke signals.
|
22
|
+
#
|
23
|
+
# Should our Redis rituals summon anything else, we pause. We ponder. We
|
24
|
+
# possibly panic. For the unexpected in Redis-land is like finding a penguin
|
25
|
+
# in your pasta - delightfully confusing, but probably not what you ordered.
|
26
|
+
#
|
27
|
+
# May your Redis returns be ever valid, and your data ever flowing!
|
28
|
+
#
|
29
|
+
@valid_command_return_values = ["OK", true, 1, 0, nil]
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_accessor :valid_command_return_values
|
33
|
+
end
|
34
|
+
|
9
35
|
# Serialization: Where Objects Go to Become Strings (and Vice Versa)!
|
10
36
|
#
|
11
37
|
# This module is chock-full of methods that'll make your head spin (in a
|
@@ -99,7 +125,7 @@ module Familia
|
|
99
125
|
# @note This method will leave breadcrumbs (traces) if you're in debug mode.
|
100
126
|
# It's like Hansel and Gretel, but for data operations!
|
101
127
|
#
|
102
|
-
def save
|
128
|
+
def save update_expiration: true
|
103
129
|
Familia.trace :SAVE, redis, redisuri, caller(1..1) if Familia.debug?
|
104
130
|
|
105
131
|
# Update our object's life story
|
@@ -108,12 +134,14 @@ module Familia
|
|
108
134
|
self.created ||= Familia.now.to_i
|
109
135
|
|
110
136
|
# Commit our tale to the Redis chronicles
|
111
|
-
|
137
|
+
#
|
138
|
+
# e.g. `ret` # => MultiResult.new(true, ["OK", "OK"])
|
139
|
+
ret = commit_fields(update_expiration: update_expiration)
|
112
140
|
|
113
141
|
Familia.ld "[save] #{self.class} #{rediskey} #{ret}"
|
114
142
|
|
115
143
|
# Did Redis accept our offering?
|
116
|
-
ret.
|
144
|
+
ret.successful?
|
117
145
|
end
|
118
146
|
|
119
147
|
# Apply a smattering of fields to this object like fairy dust.
|
@@ -126,10 +154,10 @@ module Familia
|
|
126
154
|
# fresh attributes.
|
127
155
|
#
|
128
156
|
# @example Giving your object a makeover
|
129
|
-
# dragon.apply_fields(name: "Puff", breathes: "fire", loves: "
|
157
|
+
# dragon.apply_fields(name: "Puff", breathes: "fire", loves: "Toys
|
130
158
|
# named Jackie")
|
131
159
|
# # => #<Dragon:0x007f8a1c8b0a28 @name="Puff", @breathes="fire",
|
132
|
-
# @loves="
|
160
|
+
# @loves="Toys named Jackie">
|
133
161
|
#
|
134
162
|
def apply_fields(**fields)
|
135
163
|
fields.each do |field, value|
|
@@ -143,30 +171,75 @@ module Familia
|
|
143
171
|
#
|
144
172
|
# This method performs a sacred ritual, sending our cherished attributes
|
145
173
|
# on a journey through the ethernet to find their resting place in Redis.
|
174
|
+
# It executes a transaction that includes setting field values and,
|
175
|
+
# if applicable, updating the expiration time.
|
146
176
|
#
|
147
|
-
# @return [
|
148
|
-
#
|
177
|
+
# @return [MultiResult] A mystical object containing:
|
178
|
+
# - success: A boolean indicating if all Redis commands succeeded
|
179
|
+
# - results: An array of strings, cryptic messages from the Redis gods
|
180
|
+
#
|
181
|
+
# The MultiResult object responds to:
|
182
|
+
# - successful?: Returns the boolean success value
|
183
|
+
# - results: Returns the array of command return values
|
149
184
|
#
|
150
185
|
# @note Be warned, young programmer! This method dabbles in the arcane
|
151
186
|
# art of transactions. Side effects may include data persistence and a
|
152
|
-
# slight tingling sensation.
|
187
|
+
# slight tingling sensation. The method does not raise exceptions for
|
188
|
+
# unexpected Redis responses, but logs warnings and returns a failure status.
|
153
189
|
#
|
154
190
|
# @example Offering your changes to the Redis deities
|
155
191
|
# unicorn.name = "Charlie"
|
156
192
|
# unicorn.horn_length = "magnificent"
|
157
|
-
# unicorn.commit_fields
|
158
|
-
#
|
193
|
+
# result = unicorn.commit_fields
|
194
|
+
# if result.successful?
|
195
|
+
# puts "The Redis gods are pleased with your offering"
|
196
|
+
# p result.results # => ["OK", "OK"]
|
197
|
+
# else
|
198
|
+
# puts "The Redis gods frown upon your offering"
|
199
|
+
# p result.results # Examine the unexpected values
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# @see Familia::Horreum.valid_command_return_values for the list of
|
203
|
+
# acceptable Redis command return values.
|
204
|
+
#
|
205
|
+
# @note This method performs logging at various levels:
|
206
|
+
# - Debug: Logs the object's class, Redis key, and current state before committing
|
207
|
+
# - Warn: Logs any unexpected return values from Redis commands
|
208
|
+
# - Debug: Logs the final result, including success status and all return values
|
209
|
+
#
|
210
|
+
# @note The expiration update is only performed for classes that have
|
211
|
+
# the expiration feature enabled. For others, it's a no-op.
|
159
212
|
#
|
160
|
-
def commit_fields
|
161
|
-
Familia.ld "[
|
162
|
-
transaction do |conn|
|
213
|
+
def commit_fields update_expiration: true
|
214
|
+
Familia.ld "[commit_fields1] #{self.class} #{rediskey} #{to_h} (update_expiration: #{update_expiration})"
|
215
|
+
command_return_values = transaction do |conn|
|
163
216
|
hmset
|
164
217
|
|
165
218
|
# Only classes that have the expiration ferature enabled will
|
166
219
|
# actually set an expiration time on their keys. Otherwise
|
167
|
-
# this will be a no-op.
|
168
|
-
update_expiration
|
220
|
+
# this will be a no-op that simply logs the attempt.
|
221
|
+
self.update_expiration if update_expiration
|
169
222
|
end
|
223
|
+
|
224
|
+
# The acceptable redis command return values are defined in the
|
225
|
+
# Horreum class. This is to ensure that all commands return values
|
226
|
+
# are validated against a consistent set of values.
|
227
|
+
acceptable_values = Familia::Horreum.valid_command_return_values
|
228
|
+
|
229
|
+
# Check if all return values are valid
|
230
|
+
summary_boolean = command_return_values.uniq.all? { |value|
|
231
|
+
acceptable_values.include?(value)
|
232
|
+
}
|
233
|
+
|
234
|
+
# Log the unexpected
|
235
|
+
unless summary_boolean
|
236
|
+
unexpected_values = command_return_values.reject { |value| acceptable_values.include?(value) }
|
237
|
+
Familia.warn "[commit_fields] Unexpected return values: #{unexpected_values.inspect}"
|
238
|
+
end
|
239
|
+
|
240
|
+
Familia.ld "[commit_fields2] #{self.class} #{rediskey} #{summary_boolean}: #{command_return_values}"
|
241
|
+
|
242
|
+
MultiResult.new(summary_boolean, command_return_values)
|
170
243
|
end
|
171
244
|
|
172
245
|
# Dramatically vanquish this object from the face of Redis! (ed: delete it)
|
@@ -193,13 +266,22 @@ module Familia
|
|
193
266
|
delete!
|
194
267
|
end
|
195
268
|
|
196
|
-
#
|
197
|
-
#
|
198
|
-
# object
|
269
|
+
# The Great Redis Refresh-o-matic 3000
|
270
|
+
#
|
271
|
+
# Imagine your object as a forgetful time traveler. This method is like
|
272
|
+
# zapping it with a memory ray from Redis-topia. ZAP! New memories!
|
273
|
+
#
|
274
|
+
# WARNING: This is not a gentle mind-meld. It's more like a full brain
|
275
|
+
# transplant. Any half-baked ideas floating in your object's head? POOF!
|
276
|
+
# Gone quicker than cake at a hobbit's birthday party. Unsaved spells
|
277
|
+
# will definitely be forgotten.
|
278
|
+
#
|
279
|
+
# @return What do you get for this daring act of digital amnesia? A shiny
|
280
|
+
# list of all the brain bits that got a makeover!
|
281
|
+
#
|
282
|
+
# Remember: In the game of Redis-Refresh, you win or you... well, you
|
283
|
+
# always win, but sometimes you forget why you played in the first place.
|
199
284
|
#
|
200
|
-
# @note This is a destructive operation that will overwrite any unsaved
|
201
|
-
# changes.
|
202
|
-
# @return The list of field names that were updated.
|
203
285
|
def refresh!
|
204
286
|
Familia.trace :REFRESH, redis, redisuri, caller(1..1) if Familia.debug?
|
205
287
|
fields = hgetall
|
@@ -207,14 +289,20 @@ module Familia
|
|
207
289
|
optimistic_refresh(**fields)
|
208
290
|
end
|
209
291
|
|
210
|
-
#
|
211
|
-
#
|
212
|
-
#
|
292
|
+
# Ah, the magical refresh dance! It's like giving your object a
|
293
|
+
# sip from the fountain of youth.
|
294
|
+
#
|
295
|
+
# This method twirls your object around, dips it into the Redis pool,
|
296
|
+
# and brings it back sparkling clean and up-to-date. It's using the
|
297
|
+
# refresh! spell behind the scenes, so expect some Redis whispering.
|
298
|
+
#
|
299
|
+
# @note Caution, young Rubyist! While this method loves to play
|
300
|
+
# chain-tag with other methods, it's still got that refresh! kick.
|
301
|
+
# It'll update your object faster than you can say "matz!"
|
302
|
+
#
|
303
|
+
# @return [self] Your object, freshly bathed in Redis waters, ready
|
304
|
+
# to dance with more methods in a conga line of Ruby joy!
|
213
305
|
#
|
214
|
-
# @note While this method allows chaining, it still performs a
|
215
|
-
# destructive update like refresh!.
|
216
|
-
# @return [self] Returns the object itself after refreshing, allowing
|
217
|
-
# method chaining.
|
218
306
|
def refresh
|
219
307
|
refresh!
|
220
308
|
self
|
@@ -269,35 +357,43 @@ module Familia
|
|
269
357
|
end
|
270
358
|
end
|
271
359
|
|
272
|
-
#
|
273
|
-
#
|
274
|
-
#
|
275
|
-
#
|
276
|
-
#
|
277
|
-
# -
|
278
|
-
#
|
279
|
-
#
|
280
|
-
#
|
281
|
-
# -
|
282
|
-
# -
|
283
|
-
#
|
284
|
-
#
|
285
|
-
#
|
286
|
-
#
|
287
|
-
#
|
288
|
-
#
|
289
|
-
#
|
290
|
-
#
|
291
|
-
#
|
292
|
-
#
|
293
|
-
#
|
294
|
-
#
|
295
|
-
#
|
296
|
-
#
|
297
|
-
#
|
360
|
+
# Behold, the grand tale of two serialization sorcerers:
|
361
|
+
# Familia::Redistype and Familia::Horreum!
|
362
|
+
#
|
363
|
+
# These twin wizards, though cut from the same magical cloth,
|
364
|
+
# have their own unique spells for turning Ruby objects into
|
365
|
+
# Redis-friendly potions. Let's peek into their spell books:
|
366
|
+
#
|
367
|
+
# Shared Incantations:
|
368
|
+
# - Both transform various data creatures for Redis safekeeping
|
369
|
+
# - They tame wild Strings, Symbols, and those slippery Numerics
|
370
|
+
# - Secret rituals (aka custom serialization) are welcome
|
371
|
+
#
|
372
|
+
# Mystical Differences:
|
373
|
+
# - Redistype reads the future in opts[:class] tea leaves
|
374
|
+
# - Horreum prefers to interrogate types more thoroughly
|
375
|
+
# - Redistype leaves a trail of debug breadcrumbs
|
376
|
+
#
|
377
|
+
# But wait! Enter the wise Familia.distinguisher,
|
378
|
+
# a grand unifier of serialization magic!
|
379
|
+
#
|
380
|
+
# This clever mediator:
|
381
|
+
# 1. Juggles a circus of data types from both realms
|
382
|
+
# 2. Offers a 'strict_values' toggle for the type-obsessed
|
383
|
+
# 3. Welcomes custom spells via dump_method
|
384
|
+
# 4. Sprinkles debug fairy dust à la Redistype
|
385
|
+
#
|
386
|
+
# By channeling the Familia.distinguisher, we've created a
|
387
|
+
# harmonious serialization symphony, flexible enough to dance
|
388
|
+
# with any data type that shimmies our way. And should we need
|
389
|
+
# to teach it new tricks, we know just where to wave our wands!
|
390
|
+
#
|
391
|
+
# @param value [Object] The mystical object to be transformed
|
392
|
+
#
|
393
|
+
# @return [String] The transformed, Redis-ready value.
|
298
394
|
#
|
299
395
|
def to_redis(val)
|
300
|
-
prepared = Familia.distinguisher(val, false)
|
396
|
+
prepared = Familia.distinguisher(val, strict_values: false)
|
301
397
|
|
302
398
|
if prepared.nil? && val.respond_to?(dump_method)
|
303
399
|
prepared = val.send(dump_method)
|
@@ -313,6 +409,73 @@ module Familia
|
|
313
409
|
end
|
314
410
|
# End of Serialization module
|
315
411
|
|
412
|
+
# The magical MultiResult, keeper of Redis's deepest secrets!
|
413
|
+
#
|
414
|
+
# This quirky little class wraps up the outcome of a Redis "transaction"
|
415
|
+
# (or as I like to call it, a "Redis dance party") with a bow made of
|
416
|
+
# pure Ruby delight. It knows if your commands were successful and
|
417
|
+
# keeps the results safe in its pocket dimension.
|
418
|
+
#
|
419
|
+
# @attr_reader success [Boolean] The golden ticket! True if all your
|
420
|
+
# Redis wishes came true in the transaction.
|
421
|
+
# @attr_reader results [Array<String>] A mystical array of return values,
|
422
|
+
# each one a whisper from the Redis gods.
|
423
|
+
#
|
424
|
+
# @example Summoning a MultiResult from the void
|
425
|
+
# result = MultiResult.new(true, ["OK", "OK"])
|
426
|
+
#
|
427
|
+
# @example Divining the success of your Redis ritual
|
428
|
+
# if result.successful?
|
429
|
+
# puts "Huzzah! The Redis spirits smile upon you!"
|
430
|
+
# else
|
431
|
+
# puts "Alas! The Redis gremlins have conspired against us!"
|
432
|
+
# end
|
433
|
+
#
|
434
|
+
# @example Peering into the raw essence of results
|
435
|
+
# result.results.each_with_index do |value, index|
|
436
|
+
# puts "Command #{index + 1} whispered back: #{value}"
|
437
|
+
# end
|
438
|
+
#
|
439
|
+
class MultiResult
|
440
|
+
# @return [Boolean] true if all commands in the transaction succeeded,
|
441
|
+
# false otherwise
|
442
|
+
attr_reader :success
|
443
|
+
|
444
|
+
# @return [Array<String>] The raw return values from the Redis commands
|
445
|
+
attr_reader :results
|
446
|
+
|
447
|
+
# Creates a new MultiResult instance.
|
448
|
+
#
|
449
|
+
# @param success [Boolean] Whether all commands succeeded
|
450
|
+
# @param results [Array<String>] The raw results from Redis commands
|
451
|
+
def initialize(success, results)
|
452
|
+
@success = success
|
453
|
+
@results = results
|
454
|
+
end
|
455
|
+
|
456
|
+
# Returns a tuple representing the result of the transaction.
|
457
|
+
#
|
458
|
+
# @return [Array] A tuple containing the success status and the raw results.
|
459
|
+
# The success status is a boolean indicating if all commands succeeded.
|
460
|
+
# The raw results is an array of return values from the Redis commands.
|
461
|
+
#
|
462
|
+
# @example
|
463
|
+
# [true, ["OK", true, 1]]
|
464
|
+
#
|
465
|
+
def tuple
|
466
|
+
[successful?, results]
|
467
|
+
end
|
468
|
+
|
469
|
+
# Convenient method to check if the commit was successful.
|
470
|
+
#
|
471
|
+
# @return [Boolean] true if all commands succeeded, false otherwise
|
472
|
+
def successful?
|
473
|
+
@success
|
474
|
+
end
|
475
|
+
alias success? successful?
|
476
|
+
end
|
477
|
+
# End of MultiResult class
|
478
|
+
|
316
479
|
include Serialization # these become Horreum instance methods
|
317
480
|
end
|
318
481
|
end
|
@@ -14,7 +14,7 @@ module Familia
|
|
14
14
|
|
15
15
|
def redisuri(suffix = nil)
|
16
16
|
u = Familia.redisuri(self.class.uri) # returns URI::Redis
|
17
|
-
u.db
|
17
|
+
u.db = db if db # override the db if we have one
|
18
18
|
u.key = rediskey(suffix)
|
19
19
|
u
|
20
20
|
end
|
data/lib/familia/horreum.rb
CHANGED
@@ -24,7 +24,7 @@ module Familia
|
|
24
24
|
class Horreum
|
25
25
|
include Familia::Base
|
26
26
|
|
27
|
-
#
|
27
|
+
# Singleton Class Context
|
28
28
|
#
|
29
29
|
# The code within this block operates on the singleton class (also known as
|
30
30
|
# eigenclass or metaclass) of the current class. This means:
|
@@ -73,6 +73,18 @@ module Familia
|
|
73
73
|
Familia.ld "[Horreum] Initializing #{self.class}"
|
74
74
|
initialize_relatives
|
75
75
|
|
76
|
+
# Automatically add a 'key' field if it's not already defined. This ensures
|
77
|
+
# that every object horreum class has a unique identifier field. Ideally
|
78
|
+
# this logic would live somewhere else b/c we only need to call it once
|
79
|
+
# per class definition. Here it gets called every time an instance is
|
80
|
+
# instantiated/
|
81
|
+
unless self.class.fields.include?(:key)
|
82
|
+
# Define the 'key' field for this class
|
83
|
+
# This approach allows flexibility in how identifiers are generated
|
84
|
+
# while ensuring each object has a consistent way to be referenced
|
85
|
+
self.class.field :key # , default: -> { identifier }
|
86
|
+
end
|
87
|
+
|
76
88
|
# If there are positional arguments, they should be the field
|
77
89
|
# values in the order they were defined in the implementing class.
|
78
90
|
#
|
@@ -94,15 +106,6 @@ module Familia
|
|
94
106
|
# end
|
95
107
|
end
|
96
108
|
|
97
|
-
# Automatically add a 'key' field if it's not already defined
|
98
|
-
# This ensures that every object has a unique identifier
|
99
|
-
unless self.class.fields.include?(:key)
|
100
|
-
# Define the 'key' field for this class
|
101
|
-
# This approach allows flexibility in how identifiers are generated
|
102
|
-
# while ensuring each object has a consistent way to be referenced
|
103
|
-
self.class.field :key # , default: -> { identifier }
|
104
|
-
end
|
105
|
-
|
106
109
|
# Implementing classes can define an init method to do any
|
107
110
|
# additional initialization. Notice that this is called
|
108
111
|
# after the fields are set.
|
@@ -233,7 +236,7 @@ module Familia
|
|
233
236
|
end
|
234
237
|
|
235
238
|
# If the unique_id is nil, raise an error
|
236
|
-
raise Problem, "Identifier is nil for #{self.class}" if unique_id.nil?
|
239
|
+
raise Problem, "Identifier is nil for #{self.class} #{rediskey}" if unique_id.nil?
|
237
240
|
raise Problem, 'Identifier is empty' if unique_id.empty?
|
238
241
|
|
239
242
|
unique_id
|
@@ -26,19 +26,19 @@ class Familia::RedisType
|
|
26
26
|
# @raise [Familia::HighRiskFactor] If serialization fails under strict
|
27
27
|
# mode.
|
28
28
|
#
|
29
|
-
def to_redis(val, strict_values
|
29
|
+
def to_redis(val, strict_values: true)
|
30
30
|
prepared = nil
|
31
31
|
|
32
32
|
Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}>", caller(1..1) if Familia.debug?
|
33
33
|
|
34
34
|
if opts[:class]
|
35
|
-
prepared = Familia.distinguisher(opts[:class], strict_values)
|
35
|
+
prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
|
36
36
|
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared||'<nil>'}"
|
37
37
|
end
|
38
38
|
|
39
39
|
if prepared.nil?
|
40
40
|
# Enforce strict values when no class option is specified
|
41
|
-
prepared = Familia.distinguisher(val, true)
|
41
|
+
prepared = Familia.distinguisher(val, strict_values: true)
|
42
42
|
Familia.ld " from <#{val.class}> => <#{prepared.class}>"
|
43
43
|
end
|
44
44
|
|
data/lib/familia/redistype.rb
CHANGED
@@ -102,7 +102,12 @@ module Familia
|
|
102
102
|
|
103
103
|
# Apply the options to instance method setters of the same name
|
104
104
|
@opts.each do |k, v|
|
105
|
-
|
105
|
+
# Bewarde logging :parent instance here implicitly calls #to_s which for
|
106
|
+
# some classes could include the identifier which could still be nil at
|
107
|
+
# this point. This would result in a Familia::Problem being raised. So
|
108
|
+
# to be on the safe-side here until we have a better understanding of
|
109
|
+
# the issue, we'll just log the class name for each key-value pair.
|
110
|
+
Familia.ld " [setting] #{k} #{v.class}"
|
106
111
|
send(:"#{k}=", v) if respond_to? :"#{k}="
|
107
112
|
end
|
108
113
|
|
@@ -50,7 +50,7 @@ module Familia
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def score(val)
|
53
|
-
ret = redis.zscore rediskey, to_redis(val, false)
|
53
|
+
ret = redis.zscore rediskey, to_redis(val, strict_values: false)
|
54
54
|
ret&.to_f
|
55
55
|
end
|
56
56
|
alias [] score
|
@@ -63,13 +63,13 @@ module Familia
|
|
63
63
|
|
64
64
|
# rank of member +v+ when ordered lowest to highest (starts at 0)
|
65
65
|
def rank(v)
|
66
|
-
ret = redis.zrank rediskey, to_redis(v, false)
|
66
|
+
ret = redis.zrank rediskey, to_redis(v, strict_values: false)
|
67
67
|
ret&.to_i
|
68
68
|
end
|
69
69
|
|
70
70
|
# rank of member +v+ when ordered highest to lowest (starts at 0)
|
71
71
|
def revrank(v)
|
72
|
-
ret = redis.zrevrank rediskey, to_redis(v, false)
|
72
|
+
ret = redis.zrevrank rediskey, to_redis(v, strict_values: false)
|
73
73
|
ret&.to_i
|
74
74
|
end
|
75
75
|
|
@@ -208,7 +208,7 @@ module Familia
|
|
208
208
|
# the identifier and not a serialized version of the object. So either
|
209
209
|
# the value exists in the sorted set or it doesn't -- we don't need to
|
210
210
|
# raise an error if it's not found.
|
211
|
-
redis.zrem rediskey, to_redis(val, false)
|
211
|
+
redis.zrem rediskey, to_redis(val, strict_values: false)
|
212
212
|
end
|
213
213
|
alias remove delete
|
214
214
|
alias rem delete
|
data/lib/familia/utils.rb
CHANGED
@@ -38,7 +38,7 @@ module Familia
|
|
38
38
|
# # => "193tosc85k3u513do2mtmibchpd2ruh5l3nsp6dnl0ov1i91h7m7"
|
39
39
|
#
|
40
40
|
def generate_id(length: 32, encoding: 36)
|
41
|
-
raise ArgumentError, "Encoding must be between 2 and
|
41
|
+
raise ArgumentError, "Encoding must be between 2 and 36" unless (1..36).include?(encoding)
|
42
42
|
|
43
43
|
input = SecureRandom.hex(length)
|
44
44
|
Digest::SHA256.hexdigest(input).to_i(16).to_s(encoding)
|
@@ -69,6 +69,7 @@ module Familia
|
|
69
69
|
# @param uri [String, URI] URI to convert
|
70
70
|
# @return [URI::Redis] Redis URI object
|
71
71
|
def redisuri(uri)
|
72
|
+
uri ||= Familia.uri
|
72
73
|
generic_uri = URI.parse(uri.to_s)
|
73
74
|
|
74
75
|
# Create a new URI::Redis object
|
@@ -77,7 +78,7 @@ module Familia
|
|
77
78
|
userinfo: generic_uri.userinfo,
|
78
79
|
host: generic_uri.host,
|
79
80
|
port: generic_uri.port,
|
80
|
-
path: generic_uri.path,
|
81
|
+
path: generic_uri.path, # the db is stored in the path
|
81
82
|
query: generic_uri.query,
|
82
83
|
fragment: generic_uri.fragment
|
83
84
|
)
|
@@ -124,37 +125,60 @@ module Familia
|
|
124
125
|
DIGEST_CLASS.hexdigest(concatenated_string)
|
125
126
|
end
|
126
127
|
|
127
|
-
# This method determines the appropriate
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
131
|
-
#
|
128
|
+
# This method determines the appropriate transformation to apply based on
|
129
|
+
# the class of the input argument.
|
130
|
+
#
|
131
|
+
# @param [Object] value_to_distinguish The value to be processed. Keep in
|
132
|
+
# mind that all data in redis is stored as a string so whatever the type
|
133
|
+
# of the value, it will be converted to a string.
|
134
|
+
# @param [Boolean] strict_values Whether to enforce strict value handling.
|
135
|
+
# Defaults to true.
|
136
|
+
# @return [String, nil] The processed value as a string or nil for unsupported
|
137
|
+
# classes.
|
138
|
+
#
|
139
|
+
# The method uses a case statement to handle different classes:
|
140
|
+
# - For `Symbol`, `String`, `Integer`, and `Float` classes, it traces the
|
141
|
+
# operation and converts the value to a string.
|
142
|
+
# - For `Familia::Horreum` class, it traces the operation and returns the
|
143
|
+
# identifier of the value.
|
144
|
+
# - For `TrueClass`, `FalseClass`, and `NilClass`, it traces the operation and
|
145
|
+
# converts the value to a string ("true", "false", or "").
|
132
146
|
# - For any other class, it traces the operation and returns nil.
|
133
147
|
#
|
134
|
-
# Alternative names for `value_to_distinguish` could be `input_value`, `value`,
|
135
|
-
|
148
|
+
# Alternative names for `value_to_distinguish` could be `input_value`, `value`,
|
149
|
+
# or `object`.
|
150
|
+
#
|
151
|
+
def distinguisher(value_to_distinguish, strict_values: true)
|
136
152
|
case value_to_distinguish
|
137
153
|
when ::Symbol, ::String, ::Integer, ::Float
|
138
|
-
|
154
|
+
Familia.trace :TOREDIS_DISTINGUISHER, redis, "string", caller(1..1) if Familia.debug?
|
155
|
+
|
139
156
|
# Symbols and numerics are naturally serializable to strings
|
140
157
|
# so it's a relatively low risk operation.
|
141
158
|
value_to_distinguish.to_s
|
142
159
|
|
143
160
|
when ::TrueClass, ::FalseClass, ::NilClass
|
144
|
-
|
145
|
-
|
146
|
-
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
#
|
151
|
-
#
|
161
|
+
Familia.trace :TOREDIS_DISTINGUISHER, redis, "true/false/nil", caller(1..1) if Familia.debug?
|
162
|
+
|
163
|
+
# TrueClass, FalseClass, and NilClass are considered high risk because their
|
164
|
+
# original types cannot be reliably determined from their serialized string
|
165
|
+
# representations. This can lead to unexpected behavior during deserialization.
|
166
|
+
# For instance, a TrueClass value serialized as "true" might be deserialized as
|
167
|
+
# a String, causing application errors. Even more problematic, a NilClass value
|
168
|
+
# serialized as an empty string makes it impossible to distinguish between a
|
169
|
+
# nil value and an empty string upon deserialization. Such scenarios can result
|
170
|
+
# in subtle, hard-to-diagnose bugs. To mitigate these risks, we raise an
|
171
|
+
# exception when encountering these types unless the strict_values option is
|
172
|
+
# explicitly set to false.
|
152
173
|
#
|
153
174
|
raise Familia::HighRiskFactor, value_to_distinguish if strict_values
|
154
175
|
value_to_distinguish.to_s #=> "true", "false", ""
|
155
176
|
|
156
177
|
when Familia::Base, Class
|
157
|
-
|
178
|
+
Familia.trace :TOREDIS_DISTINGUISHER, redis, "base", caller(1..1) if Familia.debug?
|
179
|
+
|
180
|
+
# When called with a class we simply transform it to its name. For
|
181
|
+
# instances of Familia class, we store the identifier.
|
158
182
|
if value_to_distinguish.is_a?(Class)
|
159
183
|
value_to_distinguish.name
|
160
184
|
else
|
@@ -162,14 +186,15 @@ module Familia
|
|
162
186
|
end
|
163
187
|
|
164
188
|
else
|
165
|
-
|
189
|
+
Familia.trace :TOREDIS_DISTINGUISHER, redis, "else1 #{strict_values}", caller(1..1) if Familia.debug?
|
166
190
|
|
167
191
|
if value_to_distinguish.class.ancestors.member?(Familia::Base)
|
168
|
-
|
192
|
+
Familia.trace :TOREDIS_DISTINGUISHER, redis, "isabase", caller(1..1) if Familia.debug?
|
193
|
+
|
169
194
|
value_to_distinguish.identifier
|
170
195
|
|
171
196
|
else
|
172
|
-
|
197
|
+
Familia.trace :TOREDIS_DISTINGUISHER, redis, "else2 #{strict_values}", caller(1..1) if Familia.debug?
|
173
198
|
raise Familia::HighRiskFactor, value_to_distinguish if strict_values
|
174
199
|
nil
|
175
200
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: familia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.pre.
|
4
|
+
version: 1.0.0.pre.rc6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-08-
|
11
|
+
date: 2024-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|