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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.pre-commit-config.yaml +1 -1
- data/.rubocop.yml +75 -0
- data/.rubocop_todo.yml +63 -0
- data/Gemfile +6 -1
- data/Gemfile.lock +47 -15
- data/README.md +65 -13
- data/VERSION.yml +4 -3
- data/familia.gemspec +18 -13
- data/lib/familia/base.rb +33 -0
- data/lib/familia/connection.rb +87 -0
- data/lib/familia/core_ext.rb +119 -124
- data/lib/familia/errors.rb +33 -0
- data/lib/familia/features/api_version.rb +19 -0
- data/lib/familia/features/atomic_saves.rb +8 -0
- data/lib/familia/features/quantizer.rb +35 -0
- data/lib/familia/features/safe_dump.rb +194 -0
- data/lib/familia/features.rb +51 -0
- data/lib/familia/horreum/class_methods.rb +292 -0
- data/lib/familia/horreum/commands.rb +106 -0
- data/lib/familia/horreum/relations_management.rb +141 -0
- data/lib/familia/horreum/serialization.rb +193 -0
- data/lib/familia/horreum/settings.rb +63 -0
- data/lib/familia/horreum/utils.rb +44 -0
- data/lib/familia/horreum.rb +248 -0
- data/lib/familia/logging.rb +232 -0
- data/lib/familia/redistype/commands.rb +56 -0
- data/lib/familia/redistype/serialization.rb +110 -0
- data/lib/familia/redistype.rb +185 -0
- data/lib/familia/refinements.rb +88 -0
- data/lib/familia/settings.rb +38 -0
- data/lib/familia/types/hashkey.rb +107 -0
- data/lib/familia/types/list.rb +155 -0
- data/lib/familia/types/sorted_set.rb +234 -0
- data/lib/familia/types/string.rb +115 -0
- data/lib/familia/types/unsorted_set.rb +123 -0
- data/lib/familia/utils.rb +125 -0
- data/lib/familia/version.rb +25 -0
- data/lib/familia.rb +57 -161
- data/lib/redis_middleware.rb +109 -0
- data/try/00_familia_try.rb +5 -4
- data/try/10_familia_try.rb +21 -17
- data/try/20_redis_type_try.rb +67 -0
- data/try/{21_redis_object_zset_try.rb → 21_redis_type_zset_try.rb} +2 -2
- data/try/{22_redis_object_set_try.rb → 22_redis_type_set_try.rb} +2 -2
- data/try/{23_redis_object_list_try.rb → 23_redis_type_list_try.rb} +2 -2
- data/try/{24_redis_object_string_try.rb → 24_redis_type_string_try.rb} +6 -6
- data/try/{25_redis_object_hash_try.rb → 25_redis_type_hash_try.rb} +3 -3
- data/try/26_redis_bool_try.rb +10 -6
- data/try/27_redis_horreum_try.rb +93 -0
- data/try/30_familia_object_try.rb +21 -20
- data/try/35_feature_safedump_try.rb +83 -0
- data/try/40_customer_try.rb +140 -0
- data/try/41_customer_safedump_try.rb +86 -0
- data/try/test_helpers.rb +194 -0
- metadata +51 -47
- data/lib/familia/helpers.rb +0 -70
- data/lib/familia/object.rb +0 -533
- data/lib/familia/redisobject.rb +0 -1017
- data/lib/familia/test_helpers.rb +0 -40
- data/lib/familia/tools.rb +0 -67
- 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
|