familia 1.0.0.pre.rc7 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.pre-commit-config.yaml +1 -1
  4. data/Gemfile +3 -1
  5. data/Gemfile.lock +3 -1
  6. data/README.md +5 -5
  7. data/VERSION.yml +1 -2
  8. data/familia.gemspec +1 -2
  9. data/lib/familia/base.rb +11 -11
  10. data/lib/familia/connection.rb +18 -2
  11. data/lib/familia/errors.rb +14 -0
  12. data/lib/familia/features/expiration.rb +13 -2
  13. data/lib/familia/features/quantization.rb +1 -1
  14. data/lib/familia/horreum/class_methods.rb +31 -21
  15. data/lib/familia/horreum/commands.rb +58 -15
  16. data/lib/familia/horreum/relations_management.rb +9 -2
  17. data/lib/familia/horreum/serialization.rb +130 -29
  18. data/lib/familia/horreum.rb +69 -19
  19. data/lib/familia/redistype/commands.rb +5 -2
  20. data/lib/familia/redistype/serialization.rb +17 -16
  21. data/lib/familia/redistype/types/hashkey.rb +167 -0
  22. data/lib/familia/{types → redistype/types}/list.rb +19 -14
  23. data/lib/familia/{types → redistype/types}/sorted_set.rb +22 -19
  24. data/lib/familia/{types → redistype/types}/string.rb +8 -6
  25. data/lib/familia/{types → redistype/types}/unsorted_set.rb +16 -12
  26. data/lib/familia/redistype.rb +19 -9
  27. data/lib/familia.rb +6 -1
  28. data/try/10_familia_try.rb +1 -1
  29. data/try/20_redis_type_try.rb +1 -1
  30. data/try/21_redis_type_zset_try.rb +1 -1
  31. data/try/22_redis_type_set_try.rb +1 -1
  32. data/try/23_redis_type_list_try.rb +2 -2
  33. data/try/24_redis_type_string_try.rb +3 -3
  34. data/try/25_redis_type_hash_try.rb +1 -1
  35. data/try/26_redis_bool_try.rb +2 -2
  36. data/try/27_redis_horreum_try.rb +2 -2
  37. data/try/28_redis_horreum_serialization_try.rb +159 -0
  38. data/try/29_redis_horreum_initialization_try.rb +113 -0
  39. data/try/30_familia_object_try.rb +8 -5
  40. data/try/40_customer_try.rb +6 -6
  41. data/try/91_json_bug_try.rb +86 -0
  42. data/try/92_symbolize_try.rb +96 -0
  43. data/try/93_string_coercion_try.rb +154 -0
  44. metadata +20 -15
  45. data/lib/familia/types/hashkey.rb +0 -108
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 518b9d319e506964416166f2d84cc05d9d4a603de80b79a2f512fa9b751c4680
4
- data.tar.gz: 9f1c9737d5d141050169da6123a977921e80768f056a0da138c299ccd7716abc
3
+ metadata.gz: 4a664f87a493fffb0e362752c8f988c8de27d83008f3a796b66737e3d15e771d
4
+ data.tar.gz: cb22d85f774e40a7b656713760f2f1e31b154171620038c282ce2dbe5e88492b
5
5
  SHA512:
6
- metadata.gz: 0fed4bf1187d6d9ad477504343341c1f9a5bd59d0b7c276fbb8539086ddacfd2fa2306a55f9b2b10881cdd884ad38e6a9f392f79ed822fff270d97836d681260
7
- data.tar.gz: 448a16b7b71afa58a27f0dfd464d85c495ddd517bf9eba5c622fab6c7b364566595e34d71934a2b9cd67b522e4b53ad94289a8097c8d819c011ad7e3e61bd6fe
6
+ metadata.gz: 3942bab094053731bd0d45c47a89c7bde62d26b99339cd2fad36e5481b28f91cd3eecb17c8ef6f77e3e486858151f6d5821e973f9fb7c068431abf11634806db
7
+ data.tar.gz: 32933d4adbb991b3cb790f4a47134f7e428fa617b3f33d3068d98a2e795d2ee58b2654ac22fb913abdc46dd0f3459bcdfd50d71273a137d2f337c4da521e475d
data/.gitignore CHANGED
@@ -8,6 +8,7 @@
8
8
  *.env
9
9
  *.log
10
10
  *.md
11
+ !README.md
11
12
  *.txt
12
13
  !LICENSE.txt
13
14
  .ruby-version
@@ -64,6 +64,6 @@ repos:
64
64
  stages: [prepare-commit-msg]
65
65
  name: Link commit to Github issue
66
66
  args:
67
- - "--default=[NOJIRA]"
67
+ - "--default="
68
68
  - "--pattern=[a-zA-Z0-9]{0,10}-?[0-9]{1,5}"
69
69
  - "--template=[#{}]"
data/Gemfile CHANGED
@@ -5,7 +5,9 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  group :development, :test do
8
- gem 'pry-byebug', '~> 3.10.1', require: false
8
+ # byebug only works with MRI
9
+ gem 'byebug', '~> 11.0', require: false if RUBY_ENGINE == 'ruby'
10
+ gem 'pry-byebug', '~> 3.10.1', require: false if RUBY_ENGINE == 'ruby'
9
11
  gem 'rubocop', require: false
10
12
  gem 'rubocop-performance', require: false
11
13
  gem 'rubocop-thread_safety', require: false
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (1.0.0.pre.rc7)
4
+ familia (1.1.0.pre.rc1)
5
5
  redis (>= 4.8.1, < 6.0)
6
+ stringio (~> 3.1.1)
6
7
  uri-redis (~> 1.3)
7
8
 
8
9
  GEM
@@ -55,6 +56,7 @@ GEM
55
56
  rubocop (>= 0.90.0)
56
57
  ruby-progressbar (1.13.0)
57
58
  storable (0.10.0)
59
+ stringio (3.1.1)
58
60
  strscan (3.1.0)
59
61
  sysinfo (0.10.0)
60
62
  drydock (< 1.0)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Familia - 1.0.0-rc7 (August 2024)
1
+ # Familia - 1.1.0-rc1 (November 2024)
2
2
 
3
3
  **Organize and store Ruby objects in Redis. A powerful Ruby ORM (of sorts) for Redis.**
4
4
 
@@ -9,7 +9,7 @@ Familia provides a flexible and feature-rich way to interact with Redis using Ru
9
9
 
10
10
  Get it in one of the following ways:
11
11
 
12
- * In your Gemfile: `gem 'familia', '>= 1.0.0-rc4'`
12
+ * In your Gemfile: `gem 'familia', '>= 1.1.0-rc1'`
13
13
  * Install it by hand: `gem install familia --pre`
14
14
  * Or for development: `git clone git@github.com:delano/familia.git`
15
15
 
@@ -152,11 +152,11 @@ end
152
152
 
153
153
  ```ruby
154
154
  class ComplexObject < Familia::Horreum
155
- def to_redis
155
+ def serialize_value
156
156
  custom_serialization_method
157
157
  end
158
158
 
159
- def self.from_redis(data)
159
+ def self.deserialize_value(data)
160
160
  custom_deserialization_method(data)
161
161
  end
162
162
  end
@@ -188,7 +188,7 @@ flower.save
188
188
  ### Retrieving and Updating Objects
189
189
 
190
190
  ```ruby
191
- rose = Flower.from_identifier("rrose")
191
+ rose = Flower.find_by_id("rrose")
192
192
  rose.name = "Pink Rose"
193
193
  rose.save
194
194
  ```
data/VERSION.yml CHANGED
@@ -1,5 +1,4 @@
1
1
  ---
2
2
  :MAJOR: 1
3
- :MINOR: 0
3
+ :MINOR: 2
4
4
  :PATCH: 0
5
- :PRE: rc7
data/familia.gemspec CHANGED
@@ -20,9 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.required_ruby_version = Gem::Requirement.new('>= 2.7.8')
21
21
 
22
22
  spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
23
+ spec.add_dependency 'stringio', '~> 3.1.1'
23
24
  spec.add_dependency 'uri-redis', '~> 1.3'
24
25
 
25
- # byebug only works with MRI
26
- spec.add_development_dependency 'byebug', '~> 11.0' if RUBY_ENGINE == 'ruby'
27
26
  spec.metadata['rubygems_mfa_required'] = 'true'
28
27
  end
data/lib/familia/base.rb CHANGED
@@ -30,21 +30,21 @@ module Familia
30
30
  end
31
31
  end
32
32
 
33
- # Yo, this class is like that one friend who never checks expiration dates.
34
- # It's living life on the edge, data-style!
33
+ # Base implementation of update_expiration that maintains API compatibility
34
+ # with the :expiration feature's implementation.
35
35
  #
36
- # @param ttl [Integer, nil] Time To Live? More like Time To Laugh! This param
37
- # is here for compatibility, but it's as useful as a chocolate teapot.
36
+ # This is a no-op implementation that gets overridden by features like
37
+ # :expiration. It accepts an optional ttl parameter to maintain interface
38
+ # compatibility with the overriding implementations.
38
39
  #
39
- # @return [nil] Always returns nil. It's consistent in its laziness!
40
+ # @param ttl [Integer, nil] Time To Live in seconds (ignored in base implementation)
41
+ # @return [nil] Always returns nil
40
42
  #
41
- # @example Trying to teach an old dog new tricks
42
- # immortal_data.update_expiration(86400) # Nice try, but this data is here to stay!
43
+ # @note This is a no-op implementation. Classes that need expiration
44
+ # functionality should include the :expiration feature.
43
45
  #
44
- # @note This method is a no-op. It's like shouting into the void, but less echo-y.
45
- #
46
- def update_expiration(*)
47
- Familia.info "[update_expiration] Skipped for #{rediskey}. #{self.class} data is immortal!"
46
+ def update_expiration(ttl: nil)
47
+ Familia.ld "[update_expiration] Feature not enabled for #{self.class}. Key: #{rediskey} (caller: #{caller(1..1)})"
48
48
  nil
49
49
  end
50
50
 
@@ -6,6 +6,7 @@ require_relative '../../lib/redis_middleware'
6
6
  module Familia
7
7
  @uri = URI.parse 'redis://127.0.0.1'
8
8
  @redis_clients = {}
9
+ @redis_uri_by_class = {}
9
10
 
10
11
  # The Connection module provides Redis connection management for Familia.
11
12
  # It allows easy setup and access to Redis clients across different URIs.
@@ -25,17 +26,20 @@ module Familia
25
26
  # Establishes a connection to a Redis server.
26
27
  #
27
28
  # @param uri [String, URI, nil] The URI of the Redis server to connect to.
28
- # If nil, uses the default URI.
29
- # @return [Redis] The connected Redis client
29
+ # If nil, uses the default URI from `@redis_uri_by_class` or `Familia.uri`.
30
+ # @return [Redis] The connected Redis client.
31
+ # @raise [ArgumentError] If no URI is specified.
30
32
  # @example
31
33
  # Familia.connect('redis://localhost:6379')
32
34
  def connect(uri = nil)
33
35
  uri = URI.parse(uri) if uri.is_a?(String)
36
+ uri ||= @redis_uri_by_class[self]
34
37
  uri ||= Familia.uri
35
38
 
36
39
  raise ArgumentError, 'No URI specified' unless uri
37
40
 
38
41
  conf = uri.conf
42
+ @redis_uri_by_class[self] = uri.serverid
39
43
 
40
44
  if Familia.enable_redis_logging
41
45
  RedisLogger.logger = Familia.logger
@@ -49,6 +53,9 @@ module Familia
49
53
  end
50
54
 
51
55
  redis = Redis.new(conf)
56
+
57
+ # Close the existing connection if it exists
58
+ @redis_clients[uri.serverid].close if @redis_clients[uri.serverid]
52
59
  @redis_clients[uri.serverid] = redis
53
60
  end
54
61
 
@@ -72,6 +79,15 @@ module Familia
72
79
  @redis_clients[uri.serverid]
73
80
  end
74
81
 
82
+ # Retrieves the Redis client associated with the given class.
83
+ #
84
+ # @param klass [Class] The class for which to retrieve the Redis client.
85
+ # @return [Redis] The Redis client associated with the given class.
86
+ def redis_uri_by_class(klass)
87
+ uri = @redis_uri_by_class[klass]
88
+ connect(uri)
89
+ end
90
+
75
91
  # Sets the default URI for Redis connections.
76
92
  #
77
93
  # @param v [String, URI] The new default URI
@@ -30,4 +30,18 @@ module Familia
30
30
  "No client for #{uri.serverid}"
31
31
  end
32
32
  end
33
+
34
+ # Raised when attempting to refresh an object whose key doesn't exist in Redis
35
+ class KeyNotFoundError < Problem
36
+ attr_reader :key
37
+
38
+ def initialize(key)
39
+ @key = key
40
+ super
41
+ end
42
+
43
+ def message
44
+ "Key not found in Redis: #{key}"
45
+ end
46
+ end
33
47
  end
@@ -49,7 +49,7 @@ module Familia::Features
49
49
  # false otherwise.
50
50
  #
51
51
  # @example Setting an expiration of one day
52
- # object.update_expiration(86400)
52
+ # object.update_expiration(ttl: 86400)
53
53
  #
54
54
  # @note If TTL is set to zero, the expiration will be removed, making the
55
55
  # data persist indefinitely.
@@ -57,8 +57,19 @@ module Familia::Features
57
57
  # @raise [Familia::Problem] Raises an error if the TTL is not a non-negative
58
58
  # integer.
59
59
  #
60
- def update_expiration(ttl = nil)
60
+ def update_expiration(ttl: nil)
61
61
  ttl ||= self.ttl
62
+
63
+ if self.class.has_relations?
64
+ Familia.ld "[update_expiration] #{self.class} has relations: #{self.class.redis_types.keys}"
65
+ self.class.redis_types.each do |name, definition|
66
+ next if definition.opts[:ttl].nil?
67
+ obj = send(name)
68
+ Familia.ld "[update_expiration] Updating expiration for #{name} (#{obj.rediskey}) to #{ttl}"
69
+ obj.update_expiration(ttl: ttl)
70
+ end
71
+ end
72
+
62
73
  # It's important to raise exceptions here and not just log warnings. We
63
74
  # don't want to silently fail at setting expirations and cause data
64
75
  # retention issues (e.g. not removed in a timely fashion).
@@ -46,7 +46,7 @@ module Familia::Features
46
46
  end
47
47
 
48
48
  def qstamp(quantum = nil, pattern: nil, time: nil)
49
- self.class.qstamp(quantum || ttl, pattern: pattern, time: time)
49
+ self.class.qstamp(quantum || self.class.ttl, pattern: pattern, time: time)
50
50
  end
51
51
 
52
52
  extend ClassMethods
@@ -33,9 +33,6 @@ module Familia
33
33
  include Familia::Settings
34
34
  include Familia::Horreum::RelationsManagement
35
35
 
36
- attr_accessor :parent
37
- attr_writer :redis, :dump_method, :load_method
38
-
39
36
  # Returns the Redis connection for the class.
40
37
  #
41
38
  # This method retrieves the Redis connection instance for the class. If no
@@ -156,7 +153,7 @@ module Familia
156
153
  Familia.trace :FAST_WRITER, redis, "#{name}: #{val.inspect}", caller(1..1) if Familia.debug?
157
154
 
158
155
  # Convert the provided value to a format suitable for Redis storage.
159
- prepared = to_redis(val)
156
+ prepared = serialize_value(val)
160
157
  Familia.ld "[.fast_attribute!] #{name} val: #{val.class} prepared: #{prepared.class}"
161
158
 
162
159
  # Use the existing accessor method to set the attribute value.
@@ -196,6 +193,10 @@ module Familia
196
193
  @redis_types
197
194
  end
198
195
 
196
+ def has_relations?
197
+ @has_relations ||= false
198
+ end
199
+
199
200
  def db(v = nil)
200
201
  @db = v unless v.nil?
201
202
  @db || parent&.db
@@ -209,16 +210,20 @@ module Familia
209
210
  def all(suffix = nil)
210
211
  suffix ||= self.suffix
211
212
  # objects that could not be parsed will be nil
212
- keys(suffix).filter_map { |k| from_rediskey(k) }
213
+ keys(suffix).filter_map { |k| find_by_key(k) }
213
214
  end
214
215
 
215
216
  def any?(filter = '*')
216
- size(filter) > 0
217
+ matching_keys_count(filter) > 0
217
218
  end
218
219
 
219
- def size(filter = '*')
220
+ # Returns the number of Redis keys matching the given filter pattern
221
+ # @param filter [String] Redis key pattern to match (default: '*')
222
+ # @return [Integer] Number of matching keys
223
+ def matching_keys_count(filter = '*')
220
224
  redis.keys(rediskey(filter)).compact.size
221
225
  end
226
+ alias size matching_keys_count # For backwards compatibility
222
227
 
223
228
  def suffix(a = nil, &blk)
224
229
  @suffix = a || blk if a || !blk.nil?
@@ -312,17 +317,17 @@ module Familia
312
317
  # debugging.
313
318
  #
314
319
  # @example
315
- # User.from_rediskey("user:123") # Returns a User instance if it exists,
320
+ # User.find_by_key("user:123") # Returns a User instance if it exists,
316
321
  # nil otherwise
317
322
  #
318
- def from_rediskey(objkey)
323
+ def find_by_key(objkey)
319
324
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
320
325
 
321
326
  # We use a lower-level method here b/c we're working with the
322
327
  # full key and not just the identifier.
323
328
  does_exist = redis.exists(objkey).positive?
324
329
 
325
- Familia.ld "[.from_rediskey] #{self} from key #{objkey} (exists: #{does_exist})"
330
+ Familia.ld "[.find_by_key] #{self} from key #{objkey} (exists: #{does_exist})"
326
331
  Familia.trace :FROM_KEY, redis, objkey, caller(1..1) if Familia.debug?
327
332
 
328
333
  # This is the reason for calling exists first. We want to definitively
@@ -337,6 +342,7 @@ module Familia
337
342
 
338
343
  new(**obj)
339
344
  end
345
+ alias from_rediskey find_by_key # deprecated
340
346
 
341
347
  # Retrieves and instantiates an object from Redis using its identifier.
342
348
  #
@@ -347,7 +353,7 @@ module Familia
347
353
  # @return [Object, nil] An instance of the class if found, nil otherwise.
348
354
  #
349
355
  # This method constructs the full Redis key using the provided identifier
350
- # and suffix, then delegates to `from_rediskey` for the actual retrieval and
356
+ # and suffix, then delegates to `find_by_key` for the actual retrieval and
351
357
  # instantiation.
352
358
  #
353
359
  # It's a higher-level method that abstracts away the key construction,
@@ -355,19 +361,21 @@ module Familia
355
361
  # identifier.
356
362
  #
357
363
  # @example
358
- # User.from_identifier(123) # Equivalent to User.from_rediskey("user:123:object")
364
+ # User.find_by_id(123) # Equivalent to User.find_by_key("user:123:object")
359
365
  #
360
- def from_identifier(identifier, suffix = nil)
366
+ def find_by_id(identifier, suffix = nil)
361
367
  suffix ||= self.suffix
362
368
  return nil if identifier.to_s.empty?
363
369
 
364
370
  objkey = rediskey(identifier, suffix)
365
371
 
366
- Familia.ld "[.from_identifier] #{self} from key #{objkey})"
367
- Familia.trace :FROM_IDENTIFIER, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
368
- from_rediskey objkey
372
+ Familia.ld "[.find_by_id] #{self} from key #{objkey})"
373
+ Familia.trace :FIND_BY_ID, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
374
+ find_by_key objkey
369
375
  end
370
- alias load from_identifier
376
+ alias find find_by_id
377
+ alias load find_by_id # deprecated
378
+ alias from_identifier find_by_id # deprecated
371
379
 
372
380
  # Checks if an object with the given identifier exists in Redis.
373
381
  #
@@ -399,8 +407,10 @@ module Familia
399
407
  # @param suffix [Symbol, nil] The suffix to use in the Redis key (default: class suffix).
400
408
  # @return [Boolean] true if the object was successfully destroyed, false otherwise.
401
409
  #
402
- # This method constructs the full Redis key using the provided identifier and suffix,
403
- # then removes the corresponding key from Redis.
410
+ # This method is part of Familia's high-level object lifecycle management. While `delete!`
411
+ # operates directly on Redis keys, `destroy!` operates at the object level and is used for
412
+ # ORM-style operations. Use `destroy!` when removing complete objects from the system, and
413
+ # `delete!` when working directly with Redis keys.
404
414
  #
405
415
  # @example
406
416
  # User.destroy!(123) # Removes user:123:object from Redis
@@ -412,7 +422,7 @@ module Familia
412
422
  objkey = rediskey identifier, suffix
413
423
 
414
424
  ret = redis.del objkey
415
- Familia.trace :DELETED, redis, "#{objkey}: #{ret.inspect}", caller(1..1) if Familia.debug?
425
+ Familia.trace :DESTROY!, redis, "#{objkey} #{ret.inspect}", caller(1..1) if Familia.debug?
416
426
  ret.positive?
417
427
  end
418
428
 
@@ -428,7 +438,7 @@ module Familia
428
438
  # User.find # Returns all keys matching user:*:object
429
439
  # User.find('active') # Returns all keys matching user:*:active
430
440
  #
431
- def find(suffix = '*')
441
+ def find_keys(suffix = '*')
432
442
  redis.keys(rediskey('*', suffix)) || []
433
443
  end
434
444
 
@@ -18,11 +18,39 @@ module Familia
18
18
  #
19
19
  module Commands
20
20
 
21
- def exists?
22
- # Trace output comes from the class method
23
- self.class.exists? identifier, suffix
21
+ def move(db)
22
+ redis.move rediskey, db
24
23
  end
25
24
 
25
+ # Checks if the calling object's key exists in Redis.
26
+ #
27
+ # @param check_size [Boolean] When true (default), also verifies the hash has a non-zero size.
28
+ # When false, only checks key existence regardless of content.
29
+ # @return [Boolean] Returns `true` if the key exists in Redis. When `check_size` is true,
30
+ # also requires the hash to have at least one field.
31
+ #
32
+ # @example Check existence with size validation (default behavior)
33
+ # some_object.exists? # => false for empty hashes
34
+ # some_object.exists?(check_size: true) # => false for empty hashes
35
+ #
36
+ # @example Check existence only
37
+ # some_object.exists?(check_size: false) # => true for empty hashes
38
+ #
39
+ # @note The default behavior maintains backward compatibility by treating empty hashes
40
+ # as non-existent. Use `check_size: false` for pure key existence checking.
41
+ def exists?(check_size: true)
42
+ key_exists = self.class.redis.exists?(rediskey)
43
+ return key_exists unless check_size
44
+ key_exists && !size.zero?
45
+ end
46
+
47
+ # Returns the number of fields in the main object hash
48
+ # @return [Integer] number of fields
49
+ def field_count
50
+ redis.hlen rediskey
51
+ end
52
+ alias size field_count
53
+
26
54
  # Sets a timeout on key. After the timeout has expired, the key will
27
55
  # automatically be deleted. Returns 1 if the timeout was set, 0 if key
28
56
  # does not exist or the timeout could not be set.
@@ -33,20 +61,27 @@ module Familia
33
61
  redis.expire rediskey, ttl.to_i
34
62
  end
35
63
 
64
+ # Retrieves the remaining time to live (TTL) for the object's Redis key.
65
+ #
66
+ # This method accesses the ovjects Redis client to obtain the TTL of `rediskey`.
67
+ # If debugging is enabled, it logs the TTL retrieval operation using `Familia.trace`.
68
+ #
69
+ # @return [Integer] The TTL of the key in seconds. Returns -1 if the key does not exist
70
+ # or has no associated expire time.
36
71
  def realttl
37
72
  Familia.trace :REALTTL, redis, redisuri, caller(1..1) if Familia.debug?
38
73
  redis.ttl rediskey
39
74
  end
40
75
 
41
- # Deletes a field from the hash stored at the Redis key.
76
+ # Removes a field from the hash stored at the Redis key.
42
77
  #
43
- # @param field [String] The field to delete from the hash.
78
+ # @param field [String] The field to remove from the hash.
44
79
  # @return [Integer] The number of fields that were removed from the hash (0 or 1).
45
- # @note This method is destructive, as indicated by the bang (!).
46
- def hdel!(field)
80
+ def remove_field(field)
47
81
  Familia.trace :HDEL, redis, field, caller(1..1) if Familia.debug?
48
82
  redis.hdel rediskey, field
49
83
  end
84
+ alias remove remove_field # deprecated
50
85
 
51
86
  def redistype
52
87
  Familia.trace :REDISTYPE, redis, redisuri, caller(1..1) if Familia.debug?
@@ -59,6 +94,15 @@ module Familia
59
94
  redis.rename rediskey, newkey
60
95
  end
61
96
 
97
+ # Retrieves the prefix for the current instance by delegating to its class.
98
+ #
99
+ # @return [String] The prefix associated with the class of the current instance.
100
+ # @example
101
+ # instance.prefix
102
+ def prefix
103
+ self.class.prefix
104
+ end
105
+
62
106
  # For parity with RedisType#hgetall
63
107
  def hgetall
64
108
  Familia.trace :HGETALL, redis, redisuri, caller(1..1) if Familia.debug?
@@ -78,8 +122,10 @@ module Familia
78
122
  redis.hset rediskey, field, value
79
123
  end
80
124
 
81
- def hmset
82
- redis.hmset rediskey(suffix), self.to_h
125
+ def hmset(hsh={})
126
+ hsh ||= self.to_h
127
+ Familia.trace :HMSET, redis, hsh, caller(1..1) if Familia.debug?
128
+ redis.hmset rediskey(suffix), hsh
83
129
  end
84
130
 
85
131
  def hkeys
@@ -116,11 +162,6 @@ module Familia
116
162
  end
117
163
  alias decrement decr
118
164
 
119
- def hlen
120
- redis.hlen rediskey(suffix)
121
- end
122
- alias hlength hlen
123
-
124
165
  def hstrlen(field)
125
166
  redis.hstrlen rediskey(suffix), field
126
167
  end
@@ -131,12 +172,14 @@ module Familia
131
172
  end
132
173
  alias has_key? key?
133
174
 
175
+ # Deletes the entire Redis key
176
+ # @return [Boolean] true if the key was deleted, false otherwise
134
177
  def delete!
135
178
  Familia.trace :DELETE!, redis, redisuri, caller(1..1) if Familia.debug?
136
179
  ret = redis.del rediskey
137
180
  ret.positive?
138
181
  end
139
- protected :delete!
182
+ alias clear delete!
140
183
 
141
184
  end
142
185
 
@@ -16,6 +16,10 @@ module Familia
16
16
  # Call setup_relations_accessors to initialize the feature
17
17
  #
18
18
  module RelationsManagement
19
+ # A practical flag to indicate that a Horreum member has relations,
20
+ # not just theoretically but actually at least one list/haskey/etc.
21
+ @has_relations = nil
22
+
19
23
  def self.included(base)
20
24
  base.extend(ClassMethods)
21
25
  base.setup_relations_accessors
@@ -31,14 +35,17 @@ module Familia
31
35
 
32
36
  # Dynamically define instance-level relation methods
33
37
  #
34
- # Once defined, these methods can be used at the class-level of a
38
+ # Once defined, these methods can be used at the instance-level of a
35
39
  # Familia member to define *instance-level* relations to any of the
36
40
  # RedisType types (e.g. set, list, hash, etc).
37
41
  #
38
42
  define_method :"#{kind}" do |*args|
39
43
  name, opts = *args
44
+
45
+ # As log as we have at least one relation, we can set this flag.
46
+ @has_relations = true
47
+
40
48
  attach_instance_redis_object_relation name, klass, opts
41
- redis_types[name.to_s.to_sym]
42
49
  end
43
50
  define_method :"#{kind}?" do |name|
44
51
  obj = redis_types[name.to_s.to_sym]