familia 0.10.2 → 1.0.0.pre.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) 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 +65 -13
  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 +194 -0
  19. data/lib/familia/features.rb +51 -0
  20. data/lib/familia/horreum/class_methods.rb +292 -0
  21. data/lib/familia/horreum/commands.rb +106 -0
  22. data/lib/familia/horreum/relations_management.rb +141 -0
  23. data/lib/familia/horreum/serialization.rb +193 -0
  24. data/lib/familia/horreum/settings.rb +63 -0
  25. data/lib/familia/horreum/utils.rb +44 -0
  26. data/lib/familia/horreum.rb +248 -0
  27. data/lib/familia/logging.rb +232 -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/refinements.rb +88 -0
  32. data/lib/familia/settings.rb +38 -0
  33. data/lib/familia/types/hashkey.rb +107 -0
  34. data/lib/familia/types/list.rb +155 -0
  35. data/lib/familia/types/sorted_set.rb +234 -0
  36. data/lib/familia/types/string.rb +115 -0
  37. data/lib/familia/types/unsorted_set.rb +123 -0
  38. data/lib/familia/utils.rb +125 -0
  39. data/lib/familia/version.rb +25 -0
  40. data/lib/familia.rb +57 -161
  41. data/lib/redis_middleware.rb +109 -0
  42. data/try/00_familia_try.rb +5 -4
  43. data/try/10_familia_try.rb +21 -17
  44. data/try/20_redis_type_try.rb +67 -0
  45. data/try/{21_redis_object_zset_try.rb → 21_redis_type_zset_try.rb} +2 -2
  46. data/try/{22_redis_object_set_try.rb → 22_redis_type_set_try.rb} +2 -2
  47. data/try/{23_redis_object_list_try.rb → 23_redis_type_list_try.rb} +2 -2
  48. data/try/{24_redis_object_string_try.rb → 24_redis_type_string_try.rb} +6 -6
  49. data/try/{25_redis_object_hash_try.rb → 25_redis_type_hash_try.rb} +3 -3
  50. data/try/26_redis_bool_try.rb +10 -6
  51. data/try/27_redis_horreum_try.rb +93 -0
  52. data/try/30_familia_object_try.rb +21 -20
  53. data/try/35_feature_safedump_try.rb +83 -0
  54. data/try/40_customer_try.rb +140 -0
  55. data/try/41_customer_safedump_try.rb +86 -0
  56. data/try/test_helpers.rb +194 -0
  57. metadata +51 -47
  58. data/lib/familia/helpers.rb +0 -70
  59. data/lib/familia/object.rb +0 -533
  60. data/lib/familia/redisobject.rb +0 -1017
  61. data/lib/familia/test_helpers.rb +0 -40
  62. data/lib/familia/tools.rb +0 -67
  63. data/try/20_redis_object_try.rb +0 -44
@@ -0,0 +1,232 @@
1
+ # rubocop:disable all
2
+
3
+ require 'pathname'
4
+ require 'logger'
5
+
6
+ module Familia
7
+ @logger = Logger.new($stdout)
8
+ @logger.progname = name
9
+ @logger.formatter = proc do |severity, datetime, progname, msg|
10
+ severity_letter = severity[0] # Get the first letter of the severity
11
+ pid = Process.pid
12
+ thread_id = Thread.current.object_id
13
+ full_path, line = caller[4].split(":")[0..1]
14
+ parent_path = Pathname.new(full_path).ascend.find { |p| p.basename.to_s == 'familia' }
15
+ relative_path = full_path.sub(parent_path.to_s, 'familia')
16
+ utc_datetime = datetime.utc.strftime("%m-%d %H:%M:%S.%6N")
17
+
18
+ # Get the severity letter from the thread local variable or use
19
+ # the default. The thread local variable is set in the trace
20
+ # method in the LoggerTraceRefinement module. The name of the
21
+ # variable `severity_letter` is arbitrary and could be anything.
22
+ severity_letter = Thread.current[:severity_letter] || severity_letter
23
+
24
+ "#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}: #{msg} [#{relative_path}:#{line}]\n"
25
+ end
26
+
27
+ # The Logging module provides a set of methods and constants for logging messages
28
+ # at various levels of severity. It is designed to be used with the Ruby Logger class
29
+ # to facilitate logging in applications.
30
+ #
31
+ # == Constants:
32
+ # Logger::TRACE::
33
+ # A custom log level for trace messages, typically used for very detailed
34
+ # debugging information.
35
+ #
36
+ # == Methods:
37
+ # trace::
38
+ # Logs a message at the TRACE level. This method is only available if the
39
+ # LoggerTraceRefinement is used.
40
+ #
41
+ # debug::
42
+ # Logs a message at the DEBUG level. This is used for low-level system information
43
+ # for debugging purposes.
44
+ #
45
+ # info::
46
+ # Logs a message at the INFO level. This is used for general information about
47
+ # system operation.
48
+ #
49
+ # warn::
50
+ # Logs a message at the WARN level. This is used for warning messages, typically
51
+ # for non-critical issues that require attention.
52
+ #
53
+ # error::
54
+ # Logs a message at the ERROR level. This is used for error messages, typically
55
+ # for critical issues that require immediate attention.
56
+ #
57
+ # fatal::
58
+ # Logs a message at the FATAL level. This is used for very severe error events
59
+ # that will presumably lead the application to abort.
60
+ #
61
+ # == Usage:
62
+ # To use the Logging module, you need to include the LoggerTraceRefinement module
63
+ # and use the `using` keyword to enable the refinement. This will add the TRACE
64
+ # log level and the trace method to the Logger class.
65
+ #
66
+ # Example:
67
+ # require 'logger'
68
+ #
69
+ # module LoggerTraceRefinement
70
+ # refine Logger do
71
+ # TRACE = 0
72
+ #
73
+ # def trace(progname = nil, &block)
74
+ # add(TRACE, nil, progname, &block)
75
+ # end
76
+ # end
77
+ # end
78
+ #
79
+ # using LoggerTraceRefinement
80
+ #
81
+ # logger = Logger.new(STDOUT)
82
+ # logger.trace("This is a trace message")
83
+ # logger.debug("This is a debug message")
84
+ # logger.info("This is an info message")
85
+ # logger.warn("This is a warning message")
86
+ # logger.error("This is an error message")
87
+ # logger.fatal("This is a fatal message")
88
+ #
89
+ # In this example, the LoggerTraceRefinement module is defined with a refinement
90
+ # for the Logger class. The TRACE constant and trace method are added to the Logger
91
+ # class within the refinement. The `using` keyword is used to apply the refinement
92
+ # in the scope where it's needed.
93
+ #
94
+ # == Conditions:
95
+ # The trace method and TRACE log level are only available if the LoggerTraceRefinement
96
+ # module is used with the `using` keyword. Without this, the Logger class will not
97
+ # have the trace method or the TRACE log level.
98
+ #
99
+ # == Minimum Ruby Version:
100
+ # This module requires Ruby 2.0.0 or later to use refinements.
101
+ #
102
+ module Logging
103
+ attr_reader :logger
104
+
105
+ # Gives our logger the ability to use our trace method.
106
+ using LoggerTraceRefinement if LoggerTraceRefinement::ENABLED
107
+
108
+ def info(*msg)
109
+ @logger.info(*msg)
110
+ end
111
+
112
+ def warn(*msg)
113
+ @logger.warn(*msg)
114
+ end
115
+
116
+ def ld(*msg)
117
+ return unless Familia.debug?
118
+ @logger.debug(*msg)
119
+ end
120
+
121
+ def le(*msg)
122
+ @logger.error(*msg)
123
+ end
124
+
125
+ # Logs a trace message for debugging purposes if Familia.debug? is true.
126
+ #
127
+ # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
128
+ # :FROMREDIS, :LOAD, :EXISTS).
129
+ # @param redis_instance [Object] The Redis instance being used.
130
+ # @param ident [String] An identifier or key related to the operation being
131
+ # traced.
132
+ # @param context [Array<String>, String, nil] The calling context, typically
133
+ # obtained from `caller` or `caller.first`. Default is nil.
134
+ #
135
+ # @example
136
+ # Familia.trace :LOAD, Familia.redis(uri), objkey, caller if Familia.debug?
137
+ #
138
+ #
139
+ # @return [nil]
140
+ #
141
+ def trace(label, redis_instance, ident, context = nil)
142
+ return unless LoggerTraceRefinement::ENABLED
143
+ instance_id = redis_instance&.id
144
+ codeline = if context
145
+ context = [context].flatten
146
+ context.reject! { |line| line =~ %r{lib/familia} }
147
+ context.first
148
+ end
149
+ @logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
150
+ end
151
+
152
+ end
153
+ end
154
+
155
+
156
+ __END__
157
+
158
+
159
+ ### Example 1: Basic Logging
160
+ ```ruby
161
+ require 'logger'
162
+
163
+ logger = Logger.new($stdout)
164
+ logger.info("This is an info message")
165
+ logger.warn("This is a warning message")
166
+ logger.error("This is an error message")
167
+ ```
168
+
169
+ ### Example 2: Setting Log Level
170
+ ```ruby
171
+ require 'logger'
172
+
173
+ logger = Logger.new($stdout)
174
+ logger.level = Logger::WARN
175
+
176
+ logger.debug("This is a debug message") # Will not be logged
177
+ logger.info("This is an info message") # Will not be logged
178
+ logger.warn("This is a warning message")
179
+ logger.error("This is an error message")
180
+ ```
181
+
182
+ ### Example 3: Customizing Log Format
183
+ ```ruby
184
+ require 'logger'
185
+
186
+ logger = Logger.new($stdout)
187
+ logger.formatter = proc do |severity, datetime, progname, msg|
188
+ "#{datetime}: #{severity} - #{msg}\n"
189
+ end
190
+
191
+ logger.info("This is an info message")
192
+ logger.warn("This is a warning message")
193
+ logger.error("This is an error message")
194
+ ```
195
+
196
+ ### Example 4: Logging with a Program Name
197
+ ```ruby
198
+ require 'logger'
199
+
200
+ logger = Logger.new($stdout)
201
+ logger.progname = 'Familia'
202
+
203
+ logger.info("This is an info message")
204
+ logger.warn("This is a warning message")
205
+ logger.error("This is an error message")
206
+ ```
207
+
208
+ ### Example 5: Logging with a Block
209
+ ```ruby
210
+ require 'logger'
211
+
212
+ # Calling any of the methods above with a block
213
+ # (affects only the one entry).
214
+ # Doing so can have two benefits:
215
+ #
216
+ # - Context: the block can evaluate the entire program context
217
+ # and create a context-dependent message.
218
+ # - Performance: the block is not evaluated unless the log level
219
+ # permits the entry actually to be written:
220
+ #
221
+ # logger.error { my_slow_message_generator }
222
+ #
223
+ # Contrast this with the string form, where the string is
224
+ # always evaluated, regardless of the log level:
225
+ #
226
+ # logger.error("#{my_slow_message_generator}")
227
+ logger = Logger.new($stdout)
228
+
229
+ logger.info { "This is an info message" }
230
+ logger.warn { "This is a warning message" }
231
+ logger.error { "This is an error message" }
232
+ ```
@@ -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
@@ -0,0 +1,185 @@
1
+ # rubocop:disable all
2
+
3
+ require_relative 'redistype/commands'
4
+ require_relative 'redistype/serialization'
5
+
6
+ module Familia
7
+
8
+ # RedisType - Base class for Redis data type wrappers
9
+ #
10
+ # This class provides common functionality for various Redis data types
11
+ # such as String, List, Set, SortedSet, and HashKey.
12
+ #
13
+ # @abstract Subclass and implement Redis data type specific methods
14
+ class RedisType
15
+ include Familia::Base
16
+
17
+ @registered_types = {}
18
+ @valid_options = %i[class parent ttl default db key redis]
19
+ @db = nil
20
+ @ttl = nil
21
+
22
+ class << self
23
+ attr_reader :registered_types, :valid_options
24
+ attr_accessor :parent
25
+ attr_writer :ttl, :db, :uri
26
+
27
+ # To be called inside every class that inherits RedisType
28
+ # +methname+ is the term used for the class and instance methods
29
+ # that are created for the given +klass+ (e.g. set, list, etc)
30
+ def register(klass, methname)
31
+ Familia.ld "[#{self}] Registering #{klass} as #{methname}"
32
+
33
+ @registered_types[methname] = klass
34
+ end
35
+
36
+ def ttl(val = nil)
37
+ @ttl = val unless val.nil?
38
+ @ttl || parent&.ttl
39
+ end
40
+
41
+ def db(val = nil)
42
+ @db = val unless val.nil?
43
+ @db || parent&.db
44
+ end
45
+
46
+ def uri(val = nil)
47
+ @uri = val unless val.nil?
48
+ @uri || (parent ? parent.uri : Familia.uri)
49
+ end
50
+
51
+ def inherited(obj)
52
+ obj.db = db
53
+ obj.ttl = ttl
54
+ obj.uri = uri
55
+ obj.parent = self
56
+ super(obj)
57
+ end
58
+
59
+ def valid_keys_only(opts)
60
+ opts.select { |k, _| RedisType.valid_options.include? k }
61
+ end
62
+ end
63
+
64
+ attr_reader :keystring, :parent, :opts
65
+ attr_writer :dump_method, :load_method
66
+
67
+ # +keystring+: If parent is set, this will be used as the suffix
68
+ # for rediskey. Otherwise this becomes the value of the key.
69
+ # If this is an Array, the elements will be joined.
70
+ #
71
+ # Options:
72
+ #
73
+ # :class => A class that responds to Familia.load_method and
74
+ # Familia.dump_method. These will be used when loading and
75
+ # saving data from/to redis to unmarshal/marshal the class.
76
+ #
77
+ # :parent => The Familia object that this redistype object belongs
78
+ # to. This can be a class that includes Familia or an instance.
79
+ #
80
+ # :ttl => the time to live in seconds. When not nil, this will
81
+ # set the redis expire for this key whenever #save is called.
82
+ # You can also call it explicitly via #update_expiration.
83
+ #
84
+ # :default => the default value (String-only)
85
+ #
86
+ # :db => the redis database to use (ignored if :redis is used).
87
+ #
88
+ # :redis => an instance of Redis.
89
+ #
90
+ # :key => a hardcoded key to use instead of the deriving the from
91
+ # the name and parent (e.g. a derived key: customer:custid:secret_counter).
92
+ #
93
+ # Uses the redis connection of the parent or the value of
94
+ # opts[:redis] or Familia.redis (in that order).
95
+ def initialize(keystring, opts = {})
96
+ #Familia.ld " [initializing] #{self.class} #{opts}"
97
+ @keystring = keystring
98
+ @keystring = @keystring.join(Familia.delim) if @keystring.is_a?(Array)
99
+
100
+ # Remove all keys from the opts that are not in the allowed list
101
+ @opts = opts || {}
102
+ @opts = RedisType.valid_keys_only(@opts)
103
+
104
+ init if respond_to? :init
105
+ end
106
+
107
+ def redis
108
+ return @redis if @redis
109
+
110
+ parent? ? parent.redis : Familia.redis(opts[:db])
111
+ end
112
+
113
+ # Produces the full redis key for this object.
114
+ def rediskey
115
+ Familia.ld "[rediskey] #{keystring} for #{self.class} (#{opts})"
116
+
117
+ # Return the hardcoded key if it's set. This is useful for
118
+ # support legacy keys that aren't derived in the same way.
119
+ return opts[:key] if opts[:key]
120
+
121
+ if parent_instance?
122
+ # This is an instance-level redistype object so the parent instance's
123
+ # rediskey method is defined in Familia::Horreum::InstanceMethods.
124
+ parent.rediskey(keystring)
125
+ elsif parent_class?
126
+ # This is a class-level redistype object so the parent class' rediskey
127
+ # method is defined in Familia::Horreum::ClassMethods.
128
+ parent.rediskey(keystring, nil)
129
+ else
130
+ # This is a standalone RedisType object where it's keystring
131
+ # is the full key.
132
+ keystring
133
+ end
134
+ end
135
+
136
+ def class?
137
+ !@opts[:class].to_s.empty? && @opts[:class].is_a?(Familia)
138
+ end
139
+
140
+ def parent_instance?
141
+ parent.is_a?(Familia::Horreum)
142
+ end
143
+
144
+ def parent_class?
145
+ parent.is_a?(Class) && parent <= Familia::Horreum
146
+ end
147
+
148
+ def parent?
149
+ parent_class? || parent_instance?
150
+ end
151
+
152
+ def parent
153
+ @opts[:parent]
154
+ end
155
+
156
+ def ttl
157
+ @opts[:ttl] || self.class.ttl
158
+ end
159
+
160
+ def db
161
+ @opts[:db] || self.class.db
162
+ end
163
+
164
+ def uri
165
+ @opts[:uri] || self.class.uri
166
+ end
167
+
168
+ def dump_method
169
+ @dump_method || self.class.dump_method
170
+ end
171
+
172
+ def load_method
173
+ @load_method || self.class.load_method
174
+ end
175
+
176
+ include Commands
177
+ include Serialization
178
+ end
179
+
180
+ require_relative 'types/list'
181
+ require_relative 'types/unsorted_set'
182
+ require_relative 'types/sorted_set'
183
+ require_relative 'types/hashkey'
184
+ require_relative 'types/string'
185
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'logger'
5
+
6
+ # Controls whether tracing is enabled via an environment variable
7
+ FAMILIA_TRACE = ENV.fetch('FAMILIA_TRACE', 'false').downcase
8
+
9
+ # FlexibleHashAccess
10
+ #
11
+ # This module provides a refinement for the Hash class to allow flexible access
12
+ # to hash keys using either strings or symbols interchangeably for reading values.
13
+ #
14
+ # Note: This refinement only affects reading from the hash. Writing to the hash
15
+ # maintains the original key type.
16
+ #
17
+ # @example Using the refinement
18
+ # using FlexibleHashAccess
19
+ #
20
+ # h = { name: "Alice", "age" => 30 }
21
+ # h[:name] # => "Alice"
22
+ # h["name"] # => "Alice"
23
+ # h[:age] # => 30
24
+ # h["age"] # => 30
25
+ #
26
+ # h["job"] = "Developer"
27
+ # h[:job] # => "Developer"
28
+ # h["job"] # => "Developer"
29
+ #
30
+ # h[:salary] = 75000
31
+ # h[:salary] # => 75000
32
+ # h["salary"] # => nil (original key type is preserved)
33
+ #
34
+ module FlexibleHashAccess
35
+ refine Hash do
36
+ ##
37
+ # Retrieves a value from the hash using either a string or symbol key.
38
+ #
39
+ # @param key [String, Symbol] The key to look up
40
+ # @return [Object, nil] The value associated with the key, or nil if not found
41
+ def [](key)
42
+ super(key.to_s) || super(key.to_sym)
43
+ end
44
+ end
45
+ end
46
+
47
+ # LoggerTraceRefinement
48
+ #
49
+ # This module adds a 'trace' log level to the Ruby Logger class.
50
+ # It is enabled when the FAMILIA_TRACE environment variable is set to
51
+ # '1', 'true', or 'yes' (case-insensitive).
52
+ #
53
+ # @example Enabling trace logging
54
+ # # Set environment variable
55
+ # ENV['FAMILIA_TRACE'] = 'true'
56
+ #
57
+ # # In your Ruby code
58
+ # require 'logger'
59
+ # using LoggerTraceRefinement
60
+ #
61
+ # logger = Logger.new(STDOUT)
62
+ # logger.trace("This is a trace message")
63
+ #
64
+ module LoggerTraceRefinement
65
+ # Indicates whether trace logging is enabled
66
+ ENABLED = %w[1 true yes].include?(FAMILIA_TRACE)
67
+
68
+ # The numeric level for trace logging (same as DEBUG)
69
+ TRACE = 0 unless defined?(TRACE)
70
+
71
+ refine Logger do
72
+ ##
73
+ # Logs a message at the TRACE level.
74
+ #
75
+ # @param progname [String] The program name to include in the log message
76
+ # @yield A block that evaluates to the message to log
77
+ # @return [true] Always returns true
78
+ #
79
+ # @example Logging a trace message
80
+ # logger.trace("MyApp") { "Detailed trace information" }
81
+ def trace(progname = nil, &block)
82
+ Thread.current[:severity_letter] = 'T'
83
+ add(LoggerTraceRefinement::TRACE, nil, progname, &block)
84
+ ensure
85
+ Thread.current[:severity_letter] = nil
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ # rubocop:disable all
2
+
3
+ module Familia
4
+
5
+ @delim = ':'
6
+ @prefix = nil
7
+ @suffix = :object
8
+ @ttl = nil
9
+ @db = nil
10
+
11
+ module Settings
12
+
13
+ attr_writer :delim, :suffix, :ttl, :db, :prefix
14
+
15
+ def delim(val = nil)
16
+ @delim = val if val
17
+ @delim
18
+ end
19
+
20
+ def prefix(val = nil)
21
+ @prefix = val if val
22
+ @prefix
23
+ end
24
+
25
+ def suffix(val = nil)
26
+ @suffix = val if val
27
+ @suffix
28
+ end
29
+
30
+ # We define this do-nothing method because it reads better
31
+ # than simply Familia.suffix in some contexts.
32
+ def default_suffix
33
+ suffix
34
+ end
35
+
36
+ end
37
+
38
+ end