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.
- 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 +11 -12
- 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 +175 -0
- data/lib/familia/features.rb +51 -0
- data/lib/familia/horreum/class_methods.rb +240 -0
- data/lib/familia/horreum/commands.rb +59 -0
- data/lib/familia/horreum/relations_management.rb +141 -0
- data/lib/familia/horreum/serialization.rb +154 -0
- data/lib/familia/horreum/settings.rb +63 -0
- data/lib/familia/horreum/utils.rb +43 -0
- data/lib/familia/horreum.rb +198 -0
- data/lib/familia/logging.rb +249 -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/settings.rb +38 -0
- data/lib/familia/types/hashkey.rb +108 -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 +129 -0
- data/lib/familia/version.rb +25 -0
- data/lib/familia.rb +56 -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 +40 -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 +186 -0
- metadata +50 -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,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
|