familia 1.0.0.pre.rc5 → 1.0.0.pre.rc7

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: 3218c877946104986176b606caef17360ba262e22372dd61597982f9b97661db
4
- data.tar.gz: b699a7b951ccf64c3421c9f79e29f927d3df0a36a0caa005575e118ae8ad000c
3
+ metadata.gz: 518b9d319e506964416166f2d84cc05d9d4a603de80b79a2f512fa9b751c4680
4
+ data.tar.gz: 9f1c9737d5d141050169da6123a977921e80768f056a0da138c299ccd7716abc
5
5
  SHA512:
6
- metadata.gz: a19eca2cdcf6fe40e2c4109199938f95c02af3e3f6f4dda247720c983e6340ed725143a350f72b00a7c0c8d16bdab7dfb82ac4314a4d786249381f34fcb2da35
7
- data.tar.gz: 2a9b815eed8437ea6514a7d29288ecf151c371ed2013495a5eaa6e2294bf61f75c07c9946c28dbf75fcdd0df7fdc1b5890ab0d83a9354f951c0f43e7718f0df8
6
+ metadata.gz: 0fed4bf1187d6d9ad477504343341c1f9a5bd59d0b7c276fbb8539086ddacfd2fa2306a55f9b2b10881cdd884ad38e6a9f392f79ed822fff270d97836d681260
7
+ data.tar.gz: 448a16b7b71afa58a27f0dfd464d85c495ddd517bf9eba5c622fab6c7b364566595e34d71934a2b9cd67b522e4b53ad94289a8097c8d819c011ad7e3e61bd6fe
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (1.0.0.pre.rc5)
4
+ familia (1.0.0.pre.rc7)
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-rc5 (August 2024)
1
+ # Familia - 1.0.0-rc7 (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: rc5
5
+ :PRE: rc7
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
@@ -37,25 +37,25 @@ 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
60
  def update_expiration(ttl = nil)
61
61
  ttl ||= self.ttl
@@ -74,41 +74,93 @@ module Familia
74
74
  fields << name
75
75
  attr_accessor name
76
76
 
77
- # Every field gets a fast writer method for immediately persisting
78
- fast_writer! name
77
+ # Every field gets a fast attribute method for immediately persisting
78
+ fast_attribute! name
79
79
  end
80
80
 
81
- # Defines a writer method with a bang (!) suffix for a given attribute name.
81
+ # Defines a fast attribute method with a bang (!) suffix for a given
82
+ # attribute name. Fast attribute methods are used to immediately read or
83
+ # write attribute values from/to Redis. Calling a fast attribute method
84
+ # has no effect on any of the object's other attributes and does not
85
+ # trigger a call to update the object's expiration time.
82
86
  #
83
87
  # The dynamically defined method performs the following:
84
- # - Checks if the correct number of arguments is provided (exactly one).
88
+ # - Acts as both a reader and a writer method.
89
+ # - When called without arguments, retrieves the current value from Redis.
90
+ # - When called with an argument, persists the value to Redis immediately.
91
+ # - Checks if the correct number of arguments is provided (zero or one).
85
92
  # - Converts the provided value to a format suitable for Redis storage.
86
- # - Uses the existing accessor method to set the attribute value.
87
- # - Persists the value to Redis immediately using the hset command.
88
- # - Includes custom error handling to raise an ArgumentError if the wrong number of arguments is given.
89
- # - Raises a custom error message if an exception occurs during the execution of the method.
90
- #
91
- # @param [Symbol, String] name the name of the attribute for which the writer method is defined.
92
- # @raise [ArgumentError] if the wrong number of arguments is provided.
93
- # @raise [RuntimeError] if an exception occurs during the execution of the method.
94
- #
95
- def fast_writer!(name)
93
+ # - Uses the existing accessor method to set the attribute value when
94
+ # writing.
95
+ # - Persists the value to Redis immediately using the hset command when
96
+ # writing.
97
+ # - Includes custom error handling to raise an ArgumentError if the wrong
98
+ # number of arguments is given.
99
+ # - Raises a custom error message if an exception occurs during the
100
+ # execution of the method.
101
+ #
102
+ # @param [Symbol, String] name the name of the attribute for which the
103
+ # fast method is defined.
104
+ # @return [Object] the current value of the attribute when called without
105
+ # arguments.
106
+ # @raise [ArgumentError] if more than one argument is provided.
107
+ # @raise [RuntimeError] if an exception occurs during the execution of the
108
+ # method.
109
+ #
110
+ def fast_attribute!(name = nil)
111
+ # Fast attribute accessor method for the '#{name}' attribute.
112
+ # This method provides immediate read and write access to the attribute
113
+ # in Redis.
114
+ #
115
+ # When called without arguments, it retrieves the current value of the
116
+ # attribute from Redis.
117
+ # When called with an argument, it immediately persists the new value to
118
+ # Redis.
119
+ #
120
+ # @overload #{name}!
121
+ # Retrieves the current value of the attribute from Redis.
122
+ # @return [Object] the current value of the attribute.
123
+ #
124
+ # @overload #{name}!(value)
125
+ # Sets and immediately persists the new value of the attribute to
126
+ # Redis.
127
+ # @param value [Object] the new value to set for the attribute.
128
+ # @return [Object] the newly set value.
129
+ #
130
+ # @raise [ArgumentError] if more than one argument is provided.
131
+ # @raise [RuntimeError] if an exception occurs during the execution of
132
+ # the method.
133
+ #
134
+ # @note This method bypasses any object-level caching and interacts
135
+ # directly with Redis. It does not trigger updates to other attributes
136
+ # or the object's expiration time.
137
+ #
138
+ # @example
139
+ #
140
+ # def #{name}!(*args)
141
+ # # Method implementation
142
+ # end
143
+ #
96
144
  define_method :"#{name}!" do |*args|
97
145
  # Check if the correct number of arguments is provided (exactly one).
98
- raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size != 1
146
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0 or 1)" if args.size > 1
147
+
148
+ val = args.first
99
149
 
100
- value = args.first
150
+ # If no value is provided to this fast attribute method, make a call
151
+ # to redis to return the current stored value of the hash field.
152
+ return hget name if val.nil?
101
153
 
102
154
  begin
103
155
  # Trace the operation if debugging is enabled.
104
- Familia.trace :FAST_WRITER, redis, "#{name}: #{value.inspect}", caller(1..1) if Familia.debug?
156
+ Familia.trace :FAST_WRITER, redis, "#{name}: #{val.inspect}", caller(1..1) if Familia.debug?
105
157
 
106
158
  # 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}"
159
+ prepared = to_redis(val)
160
+ Familia.ld "[.fast_attribute!] #{name} val: #{val.class} prepared: #{prepared.class}"
109
161
 
110
162
  # Use the existing accessor method to set the attribute value.
111
- send :"#{name}=", value
163
+ send :"#{name}=", val
112
164
 
113
165
  # Persist the value to Redis immediately using the hset command.
114
166
  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,18 +6,26 @@ module Familia
6
6
  # Familia::Horreum
7
7
  #
8
8
  class Horreum
9
- # List of valid return values for Redis commands.
10
- # This includes:
11
- # - "OK": Indicates successful execution of a command.
12
- # - true: Indicates a successful boolean response.
13
- # - 1: Indicates success for commands that return a count of affected items.
14
- # - 0: Indicates success for commands that return a count of affected items, but no items were affected.
15
- # - nil: Indicates the absence of a value, which can be considered a valid outcome in some contexts.
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!
16
28
  #
17
- # This list is used to validate the return values of multiple Redis commands executed within methods.
18
- # Methods that run multiple Redis commands will check if all return values are included in this list
19
- # to determine overall success. If any return value is not in this list, it is considered unexpected
20
- # and may be logged or handled accordingly.
21
29
  @valid_command_return_values = ["OK", true, 1, 0, nil]
22
30
 
23
31
  class << self
@@ -117,7 +125,7 @@ module Familia
117
125
  # @note This method will leave breadcrumbs (traces) if you're in debug mode.
118
126
  # It's like Hansel and Gretel, but for data operations!
119
127
  #
120
- def save
128
+ def save update_expiration: true
121
129
  Familia.trace :SAVE, redis, redisuri, caller(1..1) if Familia.debug?
122
130
 
123
131
  # Update our object's life story
@@ -126,7 +134,9 @@ module Familia
126
134
  self.created ||= Familia.now.to_i
127
135
 
128
136
  # Commit our tale to the Redis chronicles
129
- ret = commit_fields # e.g. MultiResult.new(true, ["OK", "OK"])
137
+ #
138
+ # e.g. `ret` # => MultiResult.new(true, ["OK", "OK"])
139
+ ret = commit_fields(update_expiration: update_expiration)
130
140
 
131
141
  Familia.ld "[save] #{self.class} #{rediskey} #{ret}"
132
142
 
@@ -200,15 +210,15 @@ module Familia
200
210
  # @note The expiration update is only performed for classes that have
201
211
  # the expiration feature enabled. For others, it's a no-op.
202
212
  #
203
- def commit_fields
204
- Familia.ld "[commit_fields] #{self.class} #{rediskey} #{to_h}"
213
+ def commit_fields update_expiration: true
214
+ Familia.ld "[commit_fields1] #{self.class} #{rediskey} #{to_h} (update_expiration: #{update_expiration})"
205
215
  command_return_values = transaction do |conn|
206
216
  hmset
207
217
 
208
218
  # Only classes that have the expiration ferature enabled will
209
219
  # actually set an expiration time on their keys. Otherwise
210
- # this will be a no-op.
211
- update_expiration
220
+ # this will be a no-op that simply logs the attempt.
221
+ self.update_expiration if update_expiration
212
222
  end
213
223
 
214
224
  # The acceptable redis command return values are defined in the
@@ -224,10 +234,10 @@ module Familia
224
234
  # Log the unexpected
225
235
  unless summary_boolean
226
236
  unexpected_values = command_return_values.reject { |value| acceptable_values.include?(value) }
227
- Familia.warn "[commit_fields] Unexpected return values: #{unexpected_values}"
237
+ Familia.warn "[commit_fields] Unexpected return values: #{unexpected_values.inspect}"
228
238
  end
229
239
 
230
- Familia.ld "[commit_fields] #{self.class} #{rediskey} #{summary_boolean}: #{command_return_values}"
240
+ Familia.ld "[commit_fields2] #{self.class} #{rediskey} #{summary_boolean}: #{command_return_values}"
231
241
 
232
242
  MultiResult.new(summary_boolean, command_return_values)
233
243
  end
@@ -256,13 +266,22 @@ module Familia
256
266
  delete!
257
267
  end
258
268
 
259
- # Refreshes the object's state by querying Redis and overwriting the
260
- # current field values. This method performs a destructive update on the
261
- # 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.
262
284
  #
263
- # @note This is a destructive operation that will overwrite any unsaved
264
- # changes.
265
- # @return The list of field names that were updated.
266
285
  def refresh!
267
286
  Familia.trace :REFRESH, redis, redisuri, caller(1..1) if Familia.debug?
268
287
  fields = hgetall
@@ -270,14 +289,20 @@ module Familia
270
289
  optimistic_refresh(**fields)
271
290
  end
272
291
 
273
- # Refreshes the object's state and returns self to allow method chaining.
274
- # This method calls refresh! internally, performing the actual Redis
275
- # 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!
276
305
  #
277
- # @note While this method allows chaining, it still performs a
278
- # destructive update like refresh!.
279
- # @return [self] Returns the object itself after refreshing, allowing
280
- # method chaining.
281
306
  def refresh
282
307
  refresh!
283
308
  self
@@ -332,35 +357,43 @@ module Familia
332
357
  end
333
358
  end
334
359
 
335
- # The to_redis method in Familia::Redistype and Familia::Horreum serve
336
- # similar purposes but have some key differences in their implementation:
337
- #
338
- # Similarities:
339
- # - Both methods aim to serialize various data types for Redis storage
340
- # - Both handle basic data types like String, Symbol, and Numeric
341
- # - Both have provisions for custom serialization methods
342
- #
343
- # Differences:
344
- # - Familia::Redistype uses the opts[:class] for type hints
345
- # - Familia::Horreum had more explicit type checking and conversion
346
- # - Familia::Redistype includes more extensive debug tracing
347
- #
348
- # The centralized Familia.distinguisher method accommodates both approaches
349
- # by:
350
- # 1. Handling a wide range of data types, including those from both
351
- # implementations
352
- # 2. Providing a 'strict_values' option for flexible type handling
353
- # 3. Supporting custom serialization through a dump_method
354
- # 4. Including debug tracing similar to Familia::Redistype
355
- #
356
- # By using Familia.distinguisher, we achieve more consistent behavior
357
- # across different parts of the library while maintaining the flexibility
358
- # to handle various data types and custom serialization needs. This
359
- # centralization also makes it easier to extend or modify serialization
360
- # 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.
361
394
  #
362
395
  def to_redis(val)
363
- prepared = Familia.distinguisher(val, false)
396
+ prepared = Familia.distinguisher(val, strict_values: false)
364
397
 
365
398
  if prepared.nil? && val.respond_to?(dump_method)
366
399
  prepared = val.send(dump_method)
@@ -376,31 +409,33 @@ module Familia
376
409
  end
377
410
  # End of Serialization module
378
411
 
379
- # Represents the result of a multiple Redis commands.
412
+ # The magical MultiResult, keeper of Redis's deepest secrets!
380
413
  #
381
- # This class encapsulates the outcome of a Redis "transaction",
382
- # providing both a success indicator and the raw results from
383
- # the Redis commands executed during the transaction ("MULTI").
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.
384
418
  #
385
- # @attr_reader success [Boolean] Indicates whether all Redis commands
386
- # in the transaction were successful.
387
- # @attr_reader results [Array<String>] An array of return values from
388
- # the Redis commands executed in the transaction.
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.
389
423
  #
390
- # @example Creating a MultiResult
424
+ # @example Summoning a MultiResult from the void
391
425
  # result = MultiResult.new(true, ["OK", "OK"])
392
426
  #
393
- # @example Checking the success of a commit
427
+ # @example Divining the success of your Redis ritual
394
428
  # if result.successful?
395
- # puts "All commands succeeded"
429
+ # puts "Huzzah! The Redis spirits smile upon you!"
396
430
  # else
397
- # puts "Some commands failed"
431
+ # puts "Alas! The Redis gremlins have conspired against us!"
398
432
  # end
399
433
  #
400
- # @example Accessing raw results
434
+ # @example Peering into the raw essence of results
401
435
  # result.results.each_with_index do |value, index|
402
- # puts "Command #{index + 1} returned: #{value}"
436
+ # puts "Command #{index + 1} whispered back: #{value}"
403
437
  # end
438
+ #
404
439
  class MultiResult
405
440
  # @return [Boolean] true if all commands in the transaction succeeded,
406
441
  # false otherwise
@@ -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.
@@ -139,9 +142,11 @@ module Familia
139
142
  #
140
143
  opts[:parent] = self # unless opts.key(:parent)
141
144
 
145
+ suffix_override = opts.fetch(:suffix, name)
146
+
142
147
  # Instantiate the RedisType object and below we store it in
143
148
  # an instance variable.
144
- redis_type = klass.new name, opts
149
+ redis_type = klass.new suffix_override, opts
145
150
 
146
151
  # Freezes the redis_type, making it immutable.
147
152
  # This ensures the object's state remains consistent and prevents any modifications,
@@ -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
 
@@ -16,7 +16,7 @@ module Familia
16
16
  extend Familia::Features
17
17
 
18
18
  @registered_types = {}
19
- @valid_options = %i[class parent ttl default db key redis]
19
+ @valid_options = %i[class parent ttl default db key redis suffix]
20
20
  @db = nil
21
21
 
22
22
  feature :expiration
@@ -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
@@ -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
@@ -47,12 +47,12 @@ Familia.debug = false
47
47
  @customer.save
48
48
  #=> true
49
49
 
50
- ## Horreum object fields have a fast writer method (1 of 2)
50
+ ## Horreum object fields have a fast attribute method (1 of 2)
51
51
  Familia.trace :LOAD, @customer.redis, @customer.redisuri, caller if Familia.debug?
52
52
  @customer.name! 'Jane Doe'
53
53
  #=> 0
54
54
 
55
- ## Horreum object fields have a fast writer method (2 of 2)
55
+ ## Horreum object fields have a fast attribute method (2 of 2)
56
56
  @customer.refresh!
57
57
  @customer.name
58
58
  #=> "Jane Doe"
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.rc5
4
+ version: 1.0.0.pre.rc7
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-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis