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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b514a58bc45692f39123cce853a679078eccd78362a78facc397a1df6d94629d
4
- data.tar.gz: d29686d172bec525bee366ab54ad4e81a5903547a9a2d98c68df8ca81853c7dc
3
+ metadata.gz: 4d368f329560a1afd7252a2c8de830a7230334933cd00715b4b952e44d9fbf63
4
+ data.tar.gz: edd7b843ff07cf87aa2fe46766d7b0fd6dfa814b5404c9ca04bc42de18b05619
5
5
  SHA512:
6
- metadata.gz: cd750e1ee120eb666563e9c8c552f721926ccceaa032cd0cefcf4fad7920225ab1edc4961cf3a5a7c38d81ecab3c21b446e91a75347d51a4bc196c14f5ed94fd
7
- data.tar.gz: 6c52919952e7ce8232cf1023fafe567655580a73e4d36dd0a38b8842cb618387272db6950380f71efa01797439d9542303d3addd8c94e841255923998b93589f
6
+ metadata.gz: 0f4db87dbaa2c12636d8293a477ef1e2a000364d3e6e2515cac90dad7277efc29d3670103cdf4f7697960ec90012ddec7071004146785ec7d3c838863e1a241b
7
+ data.tar.gz: a1812fce749dc485753a10c39281feb1babab24fd95ecdd553398c4a07a800836d7bf4ac4a7574ee987d4d181842e089cd732efa343141a0bf4610594ea8f1cd
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (1.0.0.pre.rc4)
4
+ familia (1.0.0.pre.rc6)
5
5
  redis (>= 4.8.1, < 6.0)
6
6
  uri-redis (~> 1.3)
7
7
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Familia - 1.0.0-rc4 (August 2024)
1
+ # Familia - 1.0.0-rc6 (August 2024)
2
2
 
3
3
  **Organize and store Ruby objects in Redis. A powerful Ruby ORM (of sorts) for Redis.**
4
4
 
data/VERSION.yml CHANGED
@@ -2,4 +2,4 @@
2
2
  :MAJOR: 1
3
3
  :MINOR: 0
4
4
  :PATCH: 0
5
- :PRE: rc4
5
+ :PRE: rc6
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(_ = nil)
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
- # Yo, check it out! We're gonna give our Redis data an expiration date!
40
+ # Sets an expiration time for the Redis data associated with this object.
41
41
  #
42
- # It's like slapping a "Best Before" sticker on your favorite snack,
43
- # but for data. How cool is that?
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. Nil? No worries!
46
- # We'll dig up the default from our secret stash.
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] Did Redis pin that expiry note successfully?
49
- # True for "Yep!", false for "Oops, butter fingers!"
48
+ # @return [Boolean] Returns true if the expiration was set successfully,
49
+ # false otherwise.
50
50
  #
51
- # @example Teaching your pet rock the concept of mortality
52
- # rocky.update_expiration(86400) # Dwayne gets to party in Redis for one whole day!
51
+ # @example Setting an expiration of one day
52
+ # object.update_expiration(86400)
53
53
  #
54
- # @note If TTL is zero, your data gets a VIP pass to the Redis eternity club.
55
- # Fancy, huh?
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] If you try to feed it non-numbers or time-travel
58
- # (negative numbers). It's strict, but fair!
57
+ # @raise [Familia::Problem] Raises an error if the TTL is not a non-negative
58
+ # integer.
59
59
  #
60
- def update_expiration(ttl = nil)
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
- raise Familia::Problem, "TTL must be a number (#{ttl.class})" unless ttl.is_a?(Numeric)
71
- raise Familia::Problem, "TTL must be positive (#{ttl})" unless ttl.is_a?(Numeric)
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.info "[update_expiration] Expires #{rediskey} in #{ttl} seconds"
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 attribute name.
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
- value = args.first
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}: #{value.inspect}", caller(1..1) if Familia.debug?
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(value)
108
- Familia.ld "[.fast_writer!] #{name} val: #{value.class} prepared: #{prepared.class}"
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}=", value
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 automatically be deleted.
27
- # Returns 1 if the timeout was set, 0 if key does not exist or the timeout could not be set.
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
- ret = commit_fields # e.g. ["OK"]
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.uniq.all? { |value| ["OK", true].include?(value) }
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: "little boys
157
+ # dragon.apply_fields(name: "Puff", breathes: "fire", loves: "Toys
130
158
  # named Jackie")
131
159
  # # => #<Dragon:0x007f8a1c8b0a28 @name="Puff", @breathes="fire",
132
- # @loves="little boys named Jackie">
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 [Array<String>] A mystical array of strings, cryptic messages
148
- # from the Redis gods.
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
- # # => ["OK", "OK"] (The Redis gods are pleased with your offering)
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 "[commit_fields] #{self.class} #{rediskey} #{to_h}"
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
- # Refreshes the object's state by querying Redis and overwriting the
197
- # current field values. This method performs a destructive update on the
198
- # object, regardless of unsaved changes.
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
- # Refreshes the object's state and returns self to allow method chaining.
211
- # This method calls refresh! internally, performing the actual Redis
212
- # query and state update.
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
- # The to_redis method in Familia::Redistype and Familia::Horreum serve
273
- # similar purposes but have some key differences in their implementation:
274
- #
275
- # Similarities:
276
- # - Both methods aim to serialize various data types for Redis storage
277
- # - Both handle basic data types like String, Symbol, and Numeric
278
- # - Both have provisions for custom serialization methods
279
- #
280
- # Differences:
281
- # - Familia::Redistype uses the opts[:class] for type hints
282
- # - Familia::Horreum had more explicit type checking and conversion
283
- # - Familia::Redistype includes more extensive debug tracing
284
- #
285
- # The centralized Familia.distinguisher method accommodates both approaches
286
- # by:
287
- # 1. Handling a wide range of data types, including those from both
288
- # implementations
289
- # 2. Providing a 'strict_values' option for flexible type handling
290
- # 3. Supporting custom serialization through a dump_method
291
- # 4. Including debug tracing similar to Familia::Redistype
292
- #
293
- # By using Familia.distinguisher, we achieve more consistent behavior
294
- # across different parts of the library while maintaining the flexibility
295
- # to handle various data types and custom serialization needs. This
296
- # centralization also makes it easier to extend or modify serialization
297
- # behavior in the future.
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 ||= self.class.db.to_s # TODO: revisit logic (should the horrerum instance know its uri?)
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
@@ -24,7 +24,7 @@ module Familia
24
24
  class Horreum
25
25
  include Familia::Base
26
26
 
27
- # == Singleton Class Context
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 = true)
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
 
@@ -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
- Familia.ld " [setting] #{k} #{v}"
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 32" unless (1..32).include?(encoding)
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 value to return based on the class of the input argument.
128
- # It uses a case statement to handle different classes:
129
- # - For Symbol, String, Integer, and Float classes, it traces the operation and converts the value to a string.
130
- # - For Familia::Horreum class, it traces the operation and returns the identifier of the value.
131
- # - For TrueClass, FalseClass, and NilClass, it traces the operation and converts the value to a string ("true", "false", or "").
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`, or `object`.
135
- def distinguisher(value_to_distinguish, strict_values = true)
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
- #Familia.trace :TOREDIS_DISTINGUISHER, redis, "string", caller(1..1) if Familia.debug?
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
- #Familia.trace :TOREDIS_DISTINGUISHER, redis, "true/false/nil", caller(1..1) if Familia.debug?
145
- # TrueClass, FalseClass, and NilClass are high risk because we can't
146
- # reliably determine the original type of the value from the serialized
147
- # string. This can lead to unexpected behavior when deserializing. For
148
- # example, if a TrueClass value is serialized as "true" and then later
149
- # deserialized as a String, it can cause errors in the application. Worse
150
- # still, if a NilClass value is serialized as an empty string we lose the
151
- # ability to distinguish between a nil value and an empty string when
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
- #Familia.trace :TOREDIS_DISTINGUISHER, redis, "base", caller(1..1) if Familia.debug?
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
- #Familia.trace :TOREDIS_DISTINGUISHER, redis, "else1 #{strict_values}", caller(1..1) if Familia.debug?
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
- #Familia.trace :TOREDIS_DISTINGUISHER, redis, "isabase", caller(1..1) if Familia.debug?
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
- #Familia.trace :TOREDIS_DISTINGUISHER, redis, "else2 #{strict_values}", caller(1..1) if Familia.debug?
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.rc4
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-20 00:00:00.000000000 Z
11
+ date: 2024-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis