familia 0.10.2 → 1.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.pre-commit-config.yaml +1 -1
  4. data/.rubocop.yml +75 -0
  5. data/.rubocop_todo.yml +63 -0
  6. data/Gemfile +6 -1
  7. data/Gemfile.lock +47 -15
  8. data/README.md +11 -12
  9. data/VERSION.yml +4 -3
  10. data/familia.gemspec +18 -13
  11. data/lib/familia/base.rb +33 -0
  12. data/lib/familia/connection.rb +87 -0
  13. data/lib/familia/core_ext.rb +119 -124
  14. data/lib/familia/errors.rb +33 -0
  15. data/lib/familia/features/api_version.rb +19 -0
  16. data/lib/familia/features/atomic_saves.rb +8 -0
  17. data/lib/familia/features/quantizer.rb +35 -0
  18. data/lib/familia/features/safe_dump.rb +175 -0
  19. data/lib/familia/features.rb +51 -0
  20. data/lib/familia/horreum/class_methods.rb +240 -0
  21. data/lib/familia/horreum/commands.rb +59 -0
  22. data/lib/familia/horreum/relations_management.rb +141 -0
  23. data/lib/familia/horreum/serialization.rb +154 -0
  24. data/lib/familia/horreum/settings.rb +63 -0
  25. data/lib/familia/horreum/utils.rb +43 -0
  26. data/lib/familia/horreum.rb +198 -0
  27. data/lib/familia/logging.rb +249 -0
  28. data/lib/familia/redistype/commands.rb +56 -0
  29. data/lib/familia/redistype/serialization.rb +110 -0
  30. data/lib/familia/redistype.rb +185 -0
  31. data/lib/familia/settings.rb +38 -0
  32. data/lib/familia/types/hashkey.rb +108 -0
  33. data/lib/familia/types/list.rb +155 -0
  34. data/lib/familia/types/sorted_set.rb +234 -0
  35. data/lib/familia/types/string.rb +115 -0
  36. data/lib/familia/types/unsorted_set.rb +123 -0
  37. data/lib/familia/utils.rb +129 -0
  38. data/lib/familia/version.rb +25 -0
  39. data/lib/familia.rb +56 -161
  40. data/lib/redis_middleware.rb +109 -0
  41. data/try/00_familia_try.rb +5 -4
  42. data/try/10_familia_try.rb +21 -17
  43. data/try/20_redis_type_try.rb +67 -0
  44. data/try/{21_redis_object_zset_try.rb → 21_redis_type_zset_try.rb} +2 -2
  45. data/try/{22_redis_object_set_try.rb → 22_redis_type_set_try.rb} +2 -2
  46. data/try/{23_redis_object_list_try.rb → 23_redis_type_list_try.rb} +2 -2
  47. data/try/{24_redis_object_string_try.rb → 24_redis_type_string_try.rb} +6 -6
  48. data/try/{25_redis_object_hash_try.rb → 25_redis_type_hash_try.rb} +3 -3
  49. data/try/26_redis_bool_try.rb +10 -6
  50. data/try/27_redis_horreum_try.rb +40 -0
  51. data/try/30_familia_object_try.rb +21 -20
  52. data/try/35_feature_safedump_try.rb +83 -0
  53. data/try/40_customer_try.rb +140 -0
  54. data/try/41_customer_safedump_try.rb +86 -0
  55. data/try/test_helpers.rb +186 -0
  56. metadata +50 -47
  57. data/lib/familia/helpers.rb +0 -70
  58. data/lib/familia/object.rb +0 -533
  59. data/lib/familia/redisobject.rb +0 -1017
  60. data/lib/familia/test_helpers.rb +0 -40
  61. data/lib/familia/tools.rb +0 -67
  62. data/try/20_redis_object_try.rb +0 -44
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ #
5
+ # Horreum: A module for managing Redis-based object storage and relationships
6
+ #
7
+ # Key features:
8
+ # * Provides instance-level access to a single hash in Redis
9
+ # * Includes Familia for class/module level access to Redis types and operations
10
+ # * Uses 'hashkey' to define a Redis hash referred to as "object"
11
+ # * Applies a default expiry (5 years) to all keys
12
+ #
13
+ # Metaprogramming:
14
+ # * The class << self block defines class-level behavior
15
+ # * The `inherited` method extends ClassMethods to subclasses like
16
+ # `MyModel` in the example below
17
+ #
18
+ # Usage:
19
+ # class MyModel < Familia::Horreum
20
+ # field :name
21
+ # field :email
22
+ # end
23
+ #
24
+ class Horreum
25
+ include Familia::Base
26
+
27
+ # == Singleton Class Context
28
+ #
29
+ # The code within this block operates on the singleton class (also known as
30
+ # eigenclass or metaclass) of the current class. This means:
31
+ #
32
+ # 1. Methods defined here become class methods, not instance methods.
33
+ # 2. Constants and variables set here belong to the class, not instances.
34
+ # 3. This is the place to define class-level behavior and properties.
35
+ #
36
+ # Use this context for:
37
+ # * Defining class methods
38
+ # * Setting class-level configurations
39
+ # * Creating factory methods
40
+ # * Establishing relationships with other classes
41
+ #
42
+ # Example:
43
+ # class MyClass
44
+ # class << self
45
+ # def class_method
46
+ # puts "This is a class method"
47
+ # end
48
+ # end
49
+ # end
50
+ #
51
+ # MyClass.class_method # => "This is a class method"
52
+ #
53
+ # Note: Changes made here affect the class itself and all future instances,
54
+ # but not existing instances of the class.
55
+ #
56
+ class << self
57
+ # Extends ClassMethods to subclasses and tracks Familia members
58
+ def inherited(member)
59
+ Familia.trace :INHERITED, nil, "Inherited by #{member}", caller if Familia.debug?
60
+ member.extend(ClassMethods)
61
+ member.extend(Features)
62
+
63
+ # Tracks all the classes/modules that include Familia. It's
64
+ # 10pm, do you know where you Familia members are?
65
+ Familia.members << member
66
+ super
67
+ end
68
+ end
69
+
70
+ # Instance initialization
71
+ # This method sets up the object's state, including Redis-related data
72
+ def initialize(*args, **kwargs)
73
+ Familia.ld "[Horreum] Initializing #{self.class}"
74
+ initialize_relatives
75
+
76
+ # If there are positional arguments, they should be the field
77
+ # values in the order they were defined in the implementing class.
78
+ #
79
+ # Handle keyword arguments
80
+ # Fields is a known quantity, so we iterate over it rather than kwargs
81
+ # to ensure that we only set fields that are defined in the class. And
82
+ # to avoid runaways.
83
+ if args.any?
84
+ initialize_with_positional_args(*args)
85
+ elsif kwargs.any?
86
+ initialize_with_keyword_args(**kwargs)
87
+ else
88
+ Familia.ld "[Horreum] #{self.class} initialized with no arguments"
89
+ # If there are no arguments, we need to set the default values
90
+ # for the fields. This is done in the order they were defined.
91
+ # self.class.fields.each do |field|
92
+ # default = self.class.defaults[field]
93
+ # send(:"#{field}=", default) if default
94
+ # end
95
+ end
96
+
97
+ # Implementing classes can define an init method to do any
98
+ # additional initialization. Notice that this is called
99
+ # after the fields are set.
100
+ init if respond_to?(:init)
101
+ end
102
+
103
+ # Sets up related Redis objects for the instance
104
+ # This method is crucial for establishing Redis-based relationships
105
+ #
106
+ # This needs to be called in the initialize method.
107
+ #
108
+ def initialize_relatives
109
+ # Generate instances of each RedisType. These need to be
110
+ # unique for each instance of this class so they can piggyback
111
+ # on the specifc index of this instance.
112
+ #
113
+ # i.e.
114
+ # familia_object.rediskey == v1:bone:INDEXVALUE:object
115
+ # familia_object.redis_type.rediskey == v1:bone:INDEXVALUE:name
116
+ #
117
+ # See RedisType.install_redis_type
118
+ self.class.redis_types.each_pair do |name, redis_type_definition|
119
+ klass = redis_type_definition.klass
120
+ opts = redis_type_definition.opts
121
+ Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
122
+
123
+ # As a subclass of Familia::Horreum, we add ourselves as the parent
124
+ # automatically. This is what determines the rediskey for RedisType
125
+ # instance and which redis connection.
126
+ #
127
+ # e.g. If the parent's rediskey is `customer:customer_id:object`
128
+ # then the rediskey for this RedisType instance will be
129
+ # `customer:customer_id:name`.
130
+ #
131
+ opts[:parent] = self # unless opts.key(:parent)
132
+
133
+ # Instantiate the RedisType object and below we store it in
134
+ # an instance variable.
135
+ redis_type = klass.new name, opts
136
+
137
+ # Freezes the redis_type, making it immutable.
138
+ # This ensures the object's state remains consistent and prevents any modifications,
139
+ # safeguarding its integrity and making it thread-safe.
140
+ # Any attempts to change the object after this will raise a FrozenError.
141
+ redis_type.freeze
142
+
143
+ # e.g. customer.name #=> `#<Familia::HashKey:0x0000...>`
144
+ instance_variable_set :"@#{name}", redis_type
145
+ end
146
+ end
147
+
148
+ def initialize_with_positional_args(*args)
149
+ self.class.fields.zip(args).each do |field, value|
150
+ send(:"#{field}=", value) if value
151
+ end
152
+ end
153
+ private :initialize_with_positional_args
154
+
155
+ def initialize_with_keyword_args(**kwargs)
156
+ self.class.fields.each do |field|
157
+ # Redis will give us field names as strings back, but internally
158
+ # we use symbols. So we do both.
159
+ value = kwargs[field.to_sym] || kwargs[field.to_s]
160
+ send(:"#{field}=", value) if value
161
+ end
162
+ end
163
+ private :initialize_with_keyword_args
164
+
165
+ # Determines the unique identifier for the instance
166
+ # This method is used to generate Redis keys for the object
167
+ def identifier
168
+ definition = self.class.identifier # e.g.
169
+ # When definition is a symbol or string, assume it's an instance method
170
+ # to call on the object to get the unique identifier. When it's a callable
171
+ # object, call it with the object as the argument. When it's an array,
172
+ # call each method in turn and join the results. When it's nil, raise
173
+ # an error
174
+ unique_id = case definition
175
+ when Symbol, String
176
+ send(definition)
177
+ when Proc
178
+ definition.call(self)
179
+ when Array
180
+ Familia.join(definition.map { |method| send(method) })
181
+ else
182
+ raise Problem, "Invalid identifier definition: #{definition.inspect}"
183
+ end
184
+
185
+ # If the unique_id is nil, raise an error
186
+ raise Problem, "Identifier is nil for #{self}" if unique_id.nil?
187
+ raise Problem, 'Identifier is empty' if unique_id.empty?
188
+
189
+ unique_id
190
+ end
191
+ end
192
+ end
193
+
194
+ require_relative 'horreum/class_methods'
195
+ require_relative 'horreum/commands'
196
+ require_relative 'horreum/serialization'
197
+ require_relative 'horreum/settings'
198
+ require_relative 'horreum/utils'
@@ -0,0 +1,249 @@
1
+ # rubocop:disable all
2
+
3
+ require 'pathname'
4
+ require 'logger'
5
+
6
+ module LoggerTraceRefinement
7
+ # Set to same value as Logger::DEBUG since 0 is the floor
8
+ # without either more invasive changes to the Logger class
9
+ # or a CustomLogger class that inherits from Logger.
10
+ TRACE = 2 unless defined?(TRACE)
11
+ refine Logger do
12
+
13
+ def trace(progname = nil, &block)
14
+ Thread.current[:severity_letter] = 'T'
15
+ add(LoggerTraceRefinement::TRACE, nil, progname, &block)
16
+ ensure
17
+ Thread.current[:severity_letter] = nil
18
+ end
19
+
20
+ end
21
+ end
22
+
23
+ module Familia
24
+ @logger = Logger.new($stdout)
25
+ @logger.progname = name
26
+ @logger.formatter = proc do |severity, datetime, progname, msg|
27
+ severity_letter = severity[0] # Get the first letter of the severity
28
+ pid = Process.pid
29
+ thread_id = Thread.current.object_id
30
+ full_path, line = caller[4].split(":")[0..1]
31
+ parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
32
+ relative_path = full_path.sub(parent_path.to_s, 'familia')
33
+ utc_datetime = datetime.utc.strftime("%m-%d %H:%M:%S.%6N")
34
+
35
+ # Get the severity letter from the thread local variable or use
36
+ # the default. The thread local variable is set in the trace
37
+ # method in the LoggerTraceRefinement module. The name of the
38
+ # variable `severity_letter` is arbitrary and could be anything.
39
+ severity_letter = Thread.current[:severity_letter] || severity_letter
40
+
41
+ "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}: #{msg} <#{relative_path}:#{line}>\n"
42
+ end
43
+
44
+ # The Logging module provides a set of methods and constants for logging messages
45
+ # at various levels of severity. It is designed to be used with the Ruby Logger class
46
+ # to facilitate logging in applications.
47
+ #
48
+ # == Constants:
49
+ # Logger::TRACE::
50
+ # A custom log level for trace messages, typically used for very detailed
51
+ # debugging information.
52
+ #
53
+ # == Methods:
54
+ # trace::
55
+ # Logs a message at the TRACE level. This method is only available if the
56
+ # LoggerTraceRefinement is used.
57
+ #
58
+ # debug::
59
+ # Logs a message at the DEBUG level. This is used for low-level system information
60
+ # for debugging purposes.
61
+ #
62
+ # info::
63
+ # Logs a message at the INFO level. This is used for general information about
64
+ # system operation.
65
+ #
66
+ # warn::
67
+ # Logs a message at the WARN level. This is used for warning messages, typically
68
+ # for non-critical issues that require attention.
69
+ #
70
+ # error::
71
+ # Logs a message at the ERROR level. This is used for error messages, typically
72
+ # for critical issues that require immediate attention.
73
+ #
74
+ # fatal::
75
+ # Logs a message at the FATAL level. This is used for very severe error events
76
+ # that will presumably lead the application to abort.
77
+ #
78
+ # == Usage:
79
+ # To use the Logging module, you need to include the LoggerTraceRefinement module
80
+ # and use the `using` keyword to enable the refinement. This will add the TRACE
81
+ # log level and the trace method to the Logger class.
82
+ #
83
+ # Example:
84
+ # require 'logger'
85
+ #
86
+ # module LoggerTraceRefinement
87
+ # refine Logger do
88
+ # TRACE = 0
89
+ #
90
+ # def trace(progname = nil, &block)
91
+ # add(TRACE, nil, progname, &block)
92
+ # end
93
+ # end
94
+ # end
95
+ #
96
+ # using LoggerTraceRefinement
97
+ #
98
+ # logger = Logger.new(STDOUT)
99
+ # logger.trace("This is a trace message")
100
+ # logger.debug("This is a debug message")
101
+ # logger.info("This is an info message")
102
+ # logger.warn("This is a warning message")
103
+ # logger.error("This is an error message")
104
+ # logger.fatal("This is a fatal message")
105
+ #
106
+ # In this example, the LoggerTraceRefinement module is defined with a refinement
107
+ # for the Logger class. The TRACE constant and trace method are added to the Logger
108
+ # class within the refinement. The `using` keyword is used to apply the refinement
109
+ # in the scope where it's needed.
110
+ #
111
+ # == Conditions:
112
+ # The trace method and TRACE log level are only available if the LoggerTraceRefinement
113
+ # module is used with the `using` keyword. Without this, the Logger class will not
114
+ # have the trace method or the TRACE log level.
115
+ #
116
+ # == Minimum Ruby Version:
117
+ # This module requires Ruby 2.0.0 or later to use refinements.
118
+ #
119
+ module Logging
120
+ attr_reader :logger
121
+
122
+ # Gives our logger the ability to use our trace method.
123
+ #using LoggerTraceRefinement if Familia.debug
124
+
125
+ def info(*msg)
126
+ @logger.info(*msg)
127
+ end
128
+
129
+ def warn(*msg)
130
+ @logger.warn(*msg)
131
+ end
132
+
133
+ def ld(*msg)
134
+ return unless Familia.debug?
135
+ @logger.debug(*msg)
136
+ end
137
+
138
+ def le(*msg)
139
+ @logger.error(*msg)
140
+ end
141
+
142
+ # Logs a trace message for debugging purposes if Familia.debug? is true.
143
+ #
144
+ # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
145
+ # :FROMREDIS, :LOAD, :EXISTS).
146
+ # @param redis_instance [Object] The Redis instance being used.
147
+ # @param ident [String] An identifier or key related to the operation being
148
+ # traced.
149
+ # @param context [Array<String>, String, nil] The calling context, typically
150
+ # obtained from `caller` or `caller.first`. Default is nil.
151
+ #
152
+ # @example
153
+ # Familia.trace :LOAD, Familia.redis(uri), objkey, caller if Familia.debug?
154
+ #
155
+ #
156
+ # @return [nil]
157
+ #
158
+ def trace(label, redis_instance, ident, context = nil)
159
+ return unless Familia.debug? && ENV.key?('FAMILIA_TRACE')
160
+ instance_id = redis_instance&.id
161
+ codeline = if context
162
+ context = [context].flatten
163
+ context.reject! { |line| line =~ %r{lib/familia} }
164
+ context.first
165
+ end
166
+ @logger.debug format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
167
+ end
168
+
169
+ end
170
+ end
171
+
172
+
173
+ __END__
174
+
175
+
176
+ ### Example 1: Basic Logging
177
+ ```ruby
178
+ require 'logger'
179
+
180
+ logger = Logger.new($stdout)
181
+ logger.info("This is an info message")
182
+ logger.warn("This is a warning message")
183
+ logger.error("This is an error message")
184
+ ```
185
+
186
+ ### Example 2: Setting Log Level
187
+ ```ruby
188
+ require 'logger'
189
+
190
+ logger = Logger.new($stdout)
191
+ logger.level = Logger::WARN
192
+
193
+ logger.debug("This is a debug message") # Will not be logged
194
+ logger.info("This is an info message") # Will not be logged
195
+ logger.warn("This is a warning message")
196
+ logger.error("This is an error message")
197
+ ```
198
+
199
+ ### Example 3: Customizing Log Format
200
+ ```ruby
201
+ require 'logger'
202
+
203
+ logger = Logger.new($stdout)
204
+ logger.formatter = proc do |severity, datetime, progname, msg|
205
+ "#{datetime}: #{severity} - #{msg}\n"
206
+ end
207
+
208
+ logger.info("This is an info message")
209
+ logger.warn("This is a warning message")
210
+ logger.error("This is an error message")
211
+ ```
212
+
213
+ ### Example 4: Logging with a Program Name
214
+ ```ruby
215
+ require 'logger'
216
+
217
+ logger = Logger.new($stdout)
218
+ logger.progname = 'Familia'
219
+
220
+ logger.info("This is an info message")
221
+ logger.warn("This is a warning message")
222
+ logger.error("This is an error message")
223
+ ```
224
+
225
+ ### Example 5: Logging with a Block
226
+ ```ruby
227
+ require 'logger'
228
+
229
+ # Calling any of the methods above with a block
230
+ # (affects only the one entry).
231
+ # Doing so can have two benefits:
232
+ #
233
+ # - Context: the block can evaluate the entire program context
234
+ # and create a context-dependent message.
235
+ # - Performance: the block is not evaluated unless the log level
236
+ # permits the entry actually to be written:
237
+ #
238
+ # logger.error { my_slow_message_generator }
239
+ #
240
+ # Contrast this with the string form, where the string is
241
+ # always evaluated, regardless of the log level:
242
+ #
243
+ # logger.error("#{my_slow_message_generator}")
244
+ logger = Logger.new($stdout)
245
+
246
+ logger.info { "This is an info message" }
247
+ logger.warn { "This is a warning message" }
248
+ logger.error { "This is an error message" }
249
+ ```
@@ -0,0 +1,56 @@
1
+ # rubocop:disable all
2
+
3
+ class Familia::RedisType
4
+
5
+ # Must be included in all RedisType classes to provide Redis
6
+ # commands. The class must have a rediskey method.
7
+ module Commands
8
+
9
+ def move(db)
10
+ redis.move rediskey, db
11
+ end
12
+
13
+ def rename(newkey)
14
+ redis.rename rediskey, newkey
15
+ end
16
+
17
+ def renamenx(newkey)
18
+ redis.renamenx rediskey, newkey
19
+ end
20
+
21
+ def type
22
+ redis.type rediskey
23
+ end
24
+
25
+ def delete!
26
+ redis.del rediskey
27
+ end
28
+ alias clear delete!
29
+ alias del delete!
30
+
31
+ def exists?
32
+ redis.exists(rediskey) && !size.zero?
33
+ end
34
+
35
+ def realttl
36
+ redis.ttl rediskey
37
+ end
38
+
39
+ def expire(sec)
40
+ redis.expire rediskey, sec.to_i
41
+ end
42
+
43
+ def expireat(unixtime)
44
+ redis.expireat rediskey, unixtime
45
+ end
46
+
47
+ def persist
48
+ redis.persist rediskey
49
+ end
50
+
51
+ def echo(meth, trace)
52
+ redis.echo "[#{self.class}\##{meth}] #{trace} (#{@opts[:class]}\#)"
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,110 @@
1
+ # rubocop:disable all
2
+
3
+ class Familia::RedisType
4
+
5
+ module Serialization
6
+
7
+ # Serializes an individual value for storage in Redis.
8
+ #
9
+ # This method prepares a value for storage in Redis by converting it to a string representation.
10
+ # If a class option is specified, it uses that class's serialization method.
11
+ # Otherwise, it relies on the value's own `to_s` method for serialization.
12
+ #
13
+ # @param val [Object] The value to be serialized.
14
+ # @param strict_values [Boolean] Whether to enforce strict value serialization (default: true). Only applies when no class option is specified because the class option is assumed to handle its own serialization.
15
+ # @return [String] The serialized representation of the value.
16
+ #
17
+ # @note When no class option is specified, this method attempts to serialize the value directly.
18
+ # If the serialization fails, it falls back to the value's own string representation.
19
+ #
20
+ # @example With a class option
21
+ # to_redis(User.new(name: "John"), strict_values: false) #=> '{"name":"John"}'
22
+ # to_redis(nil, strict_values: false) #=> "" (empty string)
23
+ # to_redis(true, strict_values: false) #=> "true"
24
+ #
25
+ # @example Without a class option and strict values
26
+ # to_redis(123) #=> "123" (which becomes "123" in Redis)
27
+ # to_redis("hello") #=> "hello"
28
+ # to_redis(nil) # raises an exception
29
+ # to_redis(true) # raises an exception
30
+ #
31
+ # @raise [Familia::HighRiskFactor]
32
+ #
33
+ def to_redis(val, strict_values = true)
34
+ ret = nil
35
+
36
+ Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}>", caller(1..1) if Familia.debug?
37
+
38
+ if opts[:class]
39
+ ret = Familia.distinguisher(opts[:class], strict_values)
40
+ Familia.ld " from opts[class] <#{opts[:class]}>: #{ret||'<nil>'}"
41
+ end
42
+
43
+ if ret.nil?
44
+ # Enforce strict values when no class option is specified
45
+ ret = Familia.distinguisher(val, true)
46
+ Familia.ld " from value #{val}<#{val.class}>: #{ret}<#{ret.class}>"
47
+ end
48
+
49
+ Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}> => #{ret}<#{ret.class}>", caller(1..1) if Familia.debug?
50
+
51
+ Familia.warn "[#{self.class}\#to_redis] nil returned for #{opts[:class]}\##{name}" if ret.nil?
52
+ ret
53
+ end
54
+
55
+ def multi_from_redis(*values)
56
+ # Avoid using compact! here. Using compact! as the last expression in the method
57
+ # can unintentionally return nil if no changes are made, which is not desirable.
58
+ # Instead, use compact to ensure the method returns the expected value.
59
+ multi_from_redis_with_nil(*values).compact
60
+ end
61
+
62
+ # NOTE: `multi` in this method name refers to multiple values from
63
+ # redis and not the Redis server MULTI command.
64
+ def multi_from_redis_with_nil(*values)
65
+ Familia.ld "multi_from_redis: (#{@opts}) #{values}"
66
+ return [] if values.empty?
67
+ return values.flatten unless @opts[:class]
68
+
69
+ unless @opts[:class].respond_to?(load_method)
70
+ raise Familia::Problem, "No such method: #{@opts[:class]}##{load_method}"
71
+ end
72
+
73
+ values.collect! do |obj|
74
+ next if obj.nil?
75
+
76
+ val = @opts[:class].send load_method, obj
77
+ if val.nil?
78
+ Familia.ld "[#{self.class}\#multi_from_redis] nil returned for #{@opts[:class]}\##{name}"
79
+ end
80
+
81
+ val
82
+ rescue StandardError => e
83
+ Familia.info val
84
+ Familia.info "Parse error for #{rediskey} (#{load_method}): #{e.message}"
85
+ Familia.info e.backtrace
86
+ nil
87
+ end
88
+
89
+ values
90
+ end
91
+
92
+ def from_redis(val)
93
+ return @opts[:default] if val.nil?
94
+ return val unless @opts[:class]
95
+
96
+ ret = multi_from_redis val
97
+ ret&.first # return the object or nil
98
+ end
99
+
100
+ def update_expiration(ttl = nil)
101
+ ttl ||= opts[:ttl]
102
+ return if ttl.to_i.zero? # nil will be zero
103
+
104
+ Familia.ld "#{rediskey} to #{ttl}"
105
+ expire ttl.to_i
106
+ end
107
+
108
+ end
109
+
110
+ end