Module: Familia::Horreum::ClassMethods

Includes:
RelationsManagement, Settings
Defined in:
lib/familia/horreum/class_methods.rb

Overview

ClassMethods: Provides class-level functionality for Horreum

This module is extended into classes that include Familia::Horreum, providing methods for Redis operations and object management.

Key features:

  • Includes RelationsManagement for Redis-type field handling

  • Defines methods for managing fields, identifiers, and Redis keys

  • Provides utility methods for working with Redis objects

Instance Attribute Summary collapse

Attributes included from Settings

#delim

Instance Method Summary collapse

Methods included from RelationsManagement

#attach_class_redis_object_relation, #attach_instance_redis_object_relation, included

Methods included from Settings

#default_suffix

Instance Attribute Details

#dump_methodObject



335
336
337
# File 'lib/familia/horreum/class_methods.rb', line 335

def dump_method
  @dump_method || :to_json # Familia.dump_method
end

#load_methodObject



339
340
341
# File 'lib/familia/horreum/class_methods.rb', line 339

def load_method
  @load_method || :from_json # Familia.load_method
end

#parentObject

Returns the value of attribute parent.



36
37
38
# File 'lib/familia/horreum/class_methods.rb', line 36

def parent
  @parent
end

#redisRedis

Returns the Redis connection for the class.

This method retrieves the Redis connection instance for the class. If no connection is set, it initializes a new connection using the provided URI or database configuration.

Returns:

  • (Redis)

    the Redis connection instance.



47
48
49
# File 'lib/familia/horreum/class_methods.rb', line 47

def redis
  @redis || Familia.redis(uri || db)
end

Instance Method Details

#all(suffix = :object) ⇒ Object



164
165
166
167
# File 'lib/familia/horreum/class_methods.rb', line 164

def all(suffix = :object)
  # objects that could not be parsed will be nil
  keys(suffix).filter_map { |k| from_key(k) }
end

#any?(filter = '*') ⇒ Boolean

Returns:

  • (Boolean)


169
170
171
# File 'lib/familia/horreum/class_methods.rb', line 169

def any?(filter = '*')
  size(filter) > 0
end

#class_redis_typesObject



131
132
133
134
# File 'lib/familia/horreum/class_methods.rb', line 131

def class_redis_types
  @class_redis_types ||= {}
  @class_redis_types
end

#class_redis_types?(name) ⇒ Boolean

Returns:

  • (Boolean)


136
137
138
# File 'lib/familia/horreum/class_methods.rb', line 136

def class_redis_types?(name)
  class_redis_types.key? name.to_s.to_sym
end

#create(*args) ⇒ Object



187
188
189
190
191
192
193
# File 'lib/familia/horreum/class_methods.rb', line 187

def create *args
  me = from_array(*args)
  raise "#{self} exists: #{me.rediskey}" if me.exists?

  me.save
  me
end

#db(v = nil) ⇒ Object



154
155
156
157
# File 'lib/familia/horreum/class_methods.rb', line 154

def db(v = nil)
  @db = v unless v.nil?
  @db || parent&.db
end

#destroy!(identifier, suffix = :object) ⇒ Object



297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/familia/horreum/class_methods.rb', line 297

def destroy!(identifier, suffix = :object)
  return false if identifier.to_s.empty?

  objkey = rediskey identifier, suffix

  ret = redis.del objkey
  if Familia.debug?
    Familia.trace :DELETED, redis, "#{objkey}: #{ret.inspect}",
                  caller
  end
  ret.positive?
end

#exists?(identifier, suffix = :object) ⇒ Boolean

Returns:

  • (Boolean)


287
288
289
290
291
292
293
294
295
# File 'lib/familia/horreum/class_methods.rb', line 287

def exists?(identifier, suffix = :object)
  return false if identifier.to_s.empty?

  objkey = rediskey identifier, suffix

  ret = redis.exists objkey
  Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}", caller if Familia.debug?
  ret.positive?
end

#fast_writer!(name) ⇒ Object

Defines a writer method with a bang (!) suffix for a given attribute name.

The dynamically defined method performs the following:

  • Checks if the correct number of arguments is provided (exactly one).

  • Converts the provided value to a format suitable for Redis storage.

  • Uses the existing accessor method to set the attribute value.

  • Persists the value to Redis immediately using the hset command.

  • Includes custom error handling to raise an ArgumentError if the wrong number of arguments is given.

  • Raises a custom error message if an exception occurs during the execution of the method.

Parameters:

  • name (Symbol, String)

    the name of the attribute for which the writer method is defined.

Raises:

  • (ArgumentError)

    if the wrong number of arguments is provided.

  • (RuntimeError)

    if an exception occurs during the execution of the method.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/familia/horreum/class_methods.rb', line 95

def fast_writer!(name)
  define_method :"#{name}!" do |*args|
    # Check if the correct number of arguments is provided (exactly one).
    if args.size != 1
      raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
    end

    value = args.first

    begin
      # Trace the operation if debugging is enabled.
      Familia.trace :FAST_WRITER, redis, "#{name}: #{value.inspect}", caller if Familia.debug?

      # Convert the provided value to a format suitable for Redis storage.
      prepared = to_redis(value)
      Familia.ld "[.fast_writer!] #{name} val: #{value.class} prepared: #{prepared.class}"

      # Use the existing accessor method to set the attribute value.
      send :"#{name}=", value

      # Persist the value to Redis immediately using the hset command.
      hset name, prepared
    rescue Familia::Problem => e
      # Raise a custom error message if an exception occurs during the execution of the method.
      raise "#{name}! method failed: #{e.message}", e.backtrace
    end
  end
end

#field(name) ⇒ Object

Defines a field for the class and creates accessor methods.

This method defines a new field for the class, creating getter and setter instance methods similar to ‘attr_accessor`. It also generates a fast writer method for immediate persistence to Redis.

Parameters:

  • name (Symbol, String)

    the name of the field to define.



73
74
75
76
77
78
79
# File 'lib/familia/horreum/class_methods.rb', line 73

def field(name)
  fields << name
  attr_accessor name

  # Every field gets a fast writer method for immediately persisting
  fast_writer! name
end

#fieldsObject

Returns the list of field names defined for the class in the order that they were defined. i.e. ‘field :a; field :b; fields => [:a, :b]`.



126
127
128
129
# File 'lib/familia/horreum/class_methods.rb', line 126

def fields
  @fields ||= []
  @fields
end

#find(suffix = '*') ⇒ Object



310
311
312
# File 'lib/familia/horreum/class_methods.rb', line 310

def find(suffix = '*')
  redis.keys(rediskey('*', suffix)) || []
end

#from_key(objkey) ⇒ Object?

Retrieves and instantiates an object from Redis using the full object key.

This method performs a two-step process to safely retrieve and instantiate objects:

  1. It first checks if the key exists in Redis. This is crucial because:

    • It provides a definitive answer about the object’s existence.

    • It prevents ambiguity that could arise from ‘hgetall` returning an empty hash for non-existent keys, which could lead to the creation of “empty” objects.

  2. If the key exists, it retrieves the object’s data and instantiates it.

This approach ensures that we only attempt to instantiate objects that actually exist in Redis, improving reliability and simplifying debugging.

Examples:

User.from_key("user:123")  # Returns a User instance if it exists,
nil otherwise

Parameters:

  • objkey (String)

    The full Redis key for the object.

Returns:

  • (Object, nil)

    An instance of the class if the key exists, nil otherwise.

Raises:

  • (ArgumentError)

    If the provided key is empty.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/familia/horreum/class_methods.rb', line 236

def from_key(objkey)
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?

  # We use a lower-level method here b/c we're working with the
  # full key and not just the identifier.
  does_exist = redis.exists(objkey).positive?

  Familia.ld "[.from_key] #{self} from key #{objkey} (exists: #{does_exist})"
  Familia.trace :FROM_KEY, redis, objkey, caller if Familia.debug?

  # This is the reason for calling exists first. We want to definitively
  # and without any ambiguity know if the object exists in Redis. If it
  # doesn't, we return nil. If it does, we proceed to load the object.
  # Otherwise, hgetall will return an empty hash, which will be passed to
  # the constructor, which will then be annoying to debug.
  return unless does_exist

  obj = redis.hgetall(objkey) # horreum objects are persisted as redis hashes
  Familia.trace :FROM_KEY2, redis, "#{objkey}: #{obj.inspect}", caller if Familia.debug?

  new(**obj)
end

#from_redis(identifier, suffix = :object) ⇒ Object?

Retrieves and instantiates an object from Redis using its identifier.

This method constructs the full Redis key using the provided identifier and suffix, then delegates to ‘from_key` for the actual retrieval and instantiation.

It’s a higher-level method that abstracts away the key construction, making it easier to retrieve objects when you only have their identifier.

Examples:

User.from_redis(123)  # Equivalent to User.from_key("user:123:object")

Parameters:

  • identifier (String, Integer)

    The unique identifier for the object.

  • suffix (Symbol) (defaults to: :object)

    The suffix to use in the Redis key (default: :object).

Returns:

  • (Object, nil)

    An instance of the class if found, nil otherwise.



278
279
280
281
282
283
284
285
# File 'lib/familia/horreum/class_methods.rb', line 278

def from_redis(identifier, suffix = :object)
  return nil if identifier.to_s.empty?

  objkey = rediskey(identifier, suffix)
  Familia.ld "[.from_redis] #{self} from key #{objkey})"
  Familia.trace :FROM_REDIS, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
  from_key objkey
end

#identifier(val = nil) ⇒ Object

Sets or retrieves the unique identifier for the class.

This method defines or returns the unique identifier used to generate the Redis key for the object. If a value is provided, it sets the identifier; otherwise, it returns the current identifier.

Parameters:

  • val (Object) (defaults to: nil)

    the value to set as the identifier (optional).

Returns:

  • (Object)

    the current identifier.



60
61
62
63
# File 'lib/familia/horreum/class_methods.rb', line 60

def identifier(val = nil)
  @identifier = val if val
  @identifier
end

#multiget(*ids) ⇒ Object



195
196
197
198
# File 'lib/familia/horreum/class_methods.rb', line 195

def multiget(*ids)
  ids = rawmultiget(*ids)
  ids.filter_map { |json| from_json(json) }
end

#prefix(a = nil) ⇒ Object



182
183
184
185
# File 'lib/familia/horreum/class_methods.rb', line 182

def prefix(a = nil)
  @prefix = a if a
  @prefix || name.downcase.gsub('::', Familia.delim).to_sym
end

#qstamp(quantum = nil, pattern = nil, now = Familia.now) ⇒ Object



314
315
316
317
318
319
# File 'lib/familia/horreum/class_methods.rb', line 314

def qstamp(quantum = nil, pattern = nil, now = Familia.now)
  quantum ||= ttl || 10.minutes
  pattern ||= '%H%M'
  rounded = now - (now % quantum)
  Time.at(rounded).utc.strftime(pattern)
end

#rawmultiget(*ids) ⇒ Object



200
201
202
203
204
205
206
# File 'lib/familia/horreum/class_methods.rb', line 200

def rawmultiget(*ids)
  ids.collect! { |objid| rediskey(objid) }
  return [] if ids.compact.empty?

  Familia.trace :MULTIGET, redis, "#{ids.size}: #{ids}", caller if Familia.debug?
  redis.mget(*ids)
end

#redis_object?(name) ⇒ Boolean

Returns:

  • (Boolean)


140
141
142
# File 'lib/familia/horreum/class_methods.rb', line 140

def redis_object?(name)
  redis_types.key? name.to_s.to_sym
end

#redis_typesObject



144
145
146
147
# File 'lib/familia/horreum/class_methods.rb', line 144

def redis_types
  @redis_types ||= {}
  @redis_types
end

#rediskey(identifier, suffix = self.suffix) ⇒ Object

identifier can be a value or an Array of values used to create the index. We don’t enforce a default suffix; that’s left up to the instance. The suffix is used to differentiate between different types of objects.

A nil suffix will not be included in the key.

Raises:



327
328
329
330
331
332
333
# File 'lib/familia/horreum/class_methods.rb', line 327

def rediskey(identifier, suffix = self.suffix)
  Familia.ld "[.rediskey] #{identifier} for #{self} (suffix:#{suffix})"
  raise NoIdentifier, self if identifier.to_s.empty?

  identifier &&= identifier.to_s
  Familia.rediskey(prefix, identifier, suffix)
end

#size(filter = '*') ⇒ Object



173
174
175
# File 'lib/familia/horreum/class_methods.rb', line 173

def size(filter = '*')
  redis.keys(rediskey(filter)).compact.size
end

#suffix(a = nil, &blk) ⇒ Object



177
178
179
180
# File 'lib/familia/horreum/class_methods.rb', line 177

def suffix(a = nil, &blk)
  @suffix = a || blk if a || !blk.nil?
  @suffix || Familia.default_suffix
end

#ttl(v = nil) ⇒ Object



149
150
151
152
# File 'lib/familia/horreum/class_methods.rb', line 149

def ttl(v = nil)
  @ttl = v unless v.nil?
  @ttl || parent&.ttl
end

#uri(v = nil) ⇒ Object



159
160
161
162
# File 'lib/familia/horreum/class_methods.rb', line 159

def uri(v = nil)
  @uri = v unless v.nil?
  @uri || parent&.uri
end