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,193 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
#
|
3
|
+
module Familia
|
4
|
+
# InstanceMethods - Module containing instance-level methods for Familia
|
5
|
+
#
|
6
|
+
# This module is included in classes that include Familia, providing
|
7
|
+
# instance-level functionality for Redis operations and object management.
|
8
|
+
#
|
9
|
+
class Horreum
|
10
|
+
|
11
|
+
# Methods that call load and dump (InstanceMethods)
|
12
|
+
#
|
13
|
+
# Note on refresh methods:
|
14
|
+
# In this class, refresh! is the primary method that performs the Redis
|
15
|
+
# query and state update. The non-bang refresh method is provided as a
|
16
|
+
# convenience for method chaining, but still performs the same destructive
|
17
|
+
# update as refresh!. This deviates from common Ruby conventions to better
|
18
|
+
# fit the specific needs of this system.
|
19
|
+
module Serialization
|
20
|
+
#include Familia::RedisType::Serialization
|
21
|
+
|
22
|
+
attr_writer :redis
|
23
|
+
|
24
|
+
def redis
|
25
|
+
@redis || self.class.redis
|
26
|
+
end
|
27
|
+
|
28
|
+
def transaction
|
29
|
+
original_redis = self.redis
|
30
|
+
|
31
|
+
begin
|
32
|
+
redis.multi do |conn|
|
33
|
+
self.instance_variable_set(:@redis, conn)
|
34
|
+
yield(conn)
|
35
|
+
end
|
36
|
+
ensure
|
37
|
+
self.redis = original_redis
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# A thin wrapper around `commit_fields` that updates the timestamps and
|
42
|
+
# returns a boolean.
|
43
|
+
def save
|
44
|
+
Familia.trace :SAVE, redis, redisuri, caller(1..1) if Familia.debug?
|
45
|
+
|
46
|
+
# Update timestamp fields
|
47
|
+
self.key ||= self.identifier
|
48
|
+
self.updated = Familia.now.to_i
|
49
|
+
self.created ||= Familia.now.to_i
|
50
|
+
|
51
|
+
# Thr return value of commit_fields is an array of strings: ["OK"].
|
52
|
+
ret = commit_fields # e.g. ["OK"]
|
53
|
+
|
54
|
+
Familia.ld "[save] #{self.class} #{rediskey} #{ret}"
|
55
|
+
|
56
|
+
# Convert the return value to a boolean
|
57
|
+
ret.all? { |value| value == "OK" }
|
58
|
+
end
|
59
|
+
|
60
|
+
# +return: [Array<String>] The return value of the Redis multi command
|
61
|
+
def commit_fields
|
62
|
+
Familia.ld "[commit_fields] #{self.class} #{rediskey} #{to_h}"
|
63
|
+
transaction do |conn|
|
64
|
+
hmset
|
65
|
+
update_expiration
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def destroy!
|
70
|
+
Familia.trace :DESTROY, redis, redisuri, caller(1..1) if Familia.debug?
|
71
|
+
delete!
|
72
|
+
end
|
73
|
+
|
74
|
+
# Refreshes the object's state by querying Redis and overwriting the
|
75
|
+
# current field values. This method performs a destructive update on the
|
76
|
+
# object, regardless of unsaved changes.
|
77
|
+
#
|
78
|
+
# @note This is a destructive operation that will overwrite any unsaved
|
79
|
+
# changes.
|
80
|
+
# @return The list of field names that were updated.
|
81
|
+
def refresh!
|
82
|
+
Familia.trace :REFRESH, redis, redisuri, caller(1..1) if Familia.debug?
|
83
|
+
fields = hgetall
|
84
|
+
Familia.ld "[refresh!] #{self.class} #{rediskey} #{fields.keys}"
|
85
|
+
optimistic_refresh(**fields)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Refreshes the object's state and returns self to allow method chaining.
|
89
|
+
# This method calls refresh! internally, performing the actual Redis
|
90
|
+
# query and state update.
|
91
|
+
#
|
92
|
+
# @note While this method allows chaining, it still performs a
|
93
|
+
# destructive update like refresh!.
|
94
|
+
# @return [self] Returns the object itself after refreshing, allowing
|
95
|
+
# method chaining.
|
96
|
+
def refresh
|
97
|
+
refresh!
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_h
|
102
|
+
# Use self.class.fields to efficiently generate a hash
|
103
|
+
# of all the fields for this object
|
104
|
+
self.class.fields.inject({}) do |hsh, field|
|
105
|
+
val = send(field)
|
106
|
+
prepared = to_redis(val)
|
107
|
+
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared.class}"
|
108
|
+
hsh[field] = prepared
|
109
|
+
hsh
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_a
|
114
|
+
self.class.fields.map do |field|
|
115
|
+
val = send(field)
|
116
|
+
prepared = to_redis(val)
|
117
|
+
Familia.ld " [to_a] field: #{field} val: #{val.class} prepared: #{prepared.class}"
|
118
|
+
prepared
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# The to_redis method in Familia::Redistype and Familia::Horreum serve
|
123
|
+
# similar purposes but have some key differences in their implementation:
|
124
|
+
#
|
125
|
+
# Similarities:
|
126
|
+
# - Both methods aim to serialize various data types for Redis storage
|
127
|
+
# - Both handle basic data types like String, Symbol, and Numeric
|
128
|
+
# - Both have provisions for custom serialization methods
|
129
|
+
#
|
130
|
+
# Differences:
|
131
|
+
# - Familia::Redistype uses the opts[:class] for type hints
|
132
|
+
# - Familia::Horreum had more explicit type checking and conversion
|
133
|
+
# - Familia::Redistype includes more extensive debug tracing
|
134
|
+
#
|
135
|
+
# The centralized Familia.distinguisher method accommodates both approaches
|
136
|
+
# by:
|
137
|
+
# 1. Handling a wide range of data types, including those from both
|
138
|
+
# implementations
|
139
|
+
# 2. Providing a 'strict_values' option for flexible type handling
|
140
|
+
# 3. Supporting custom serialization through a dump_method
|
141
|
+
# 4. Including debug tracing similar to Familia::Redistype
|
142
|
+
#
|
143
|
+
# By using Familia.distinguisher, we achieve more consistent behavior
|
144
|
+
# across different parts of the library while maintaining the flexibility
|
145
|
+
# to handle various data types and custom serialization needs. This
|
146
|
+
# centralization also makes it easier to extend or modify serialization
|
147
|
+
# behavior in the future.
|
148
|
+
#
|
149
|
+
def to_redis(val)
|
150
|
+
prepared = Familia.distinguisher(val, false)
|
151
|
+
|
152
|
+
if prepared.nil? && val.respond_to?(dump_method)
|
153
|
+
prepared = val.send(dump_method)
|
154
|
+
end
|
155
|
+
|
156
|
+
if prepared.nil?
|
157
|
+
Familia.ld "[#{self.class}#to_redis] nil returned for #{self.class}##{name}"
|
158
|
+
end
|
159
|
+
|
160
|
+
prepared
|
161
|
+
end
|
162
|
+
|
163
|
+
def update_expiration(ttl = nil)
|
164
|
+
ttl ||= opts[:ttl]
|
165
|
+
return if ttl.to_i.zero? # nil will be zero
|
166
|
+
|
167
|
+
Familia.ld "#{rediskey} to #{ttl}"
|
168
|
+
expire ttl.to_i
|
169
|
+
end
|
170
|
+
end
|
171
|
+
# End of Serialization module
|
172
|
+
|
173
|
+
include Serialization # these become Horreum instance methods
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
__END__
|
178
|
+
|
179
|
+
# Consider adding a retry mechanism for the refresh operation
|
180
|
+
# if it fails to fetch the expected data:
|
181
|
+
def refresh_with_retry(max_attempts = 3)
|
182
|
+
attempts = 0
|
183
|
+
base = 2
|
184
|
+
while attempts < max_attempts
|
185
|
+
refresh!
|
186
|
+
return if name == "Jane Doe" # Or whatever condition indicates a successful refresh
|
187
|
+
attempts += 1
|
188
|
+
|
189
|
+
sleep_time = 0.1 * (base ** attempts)
|
190
|
+
sleep(sleep_time) # Exponential backoff
|
191
|
+
end
|
192
|
+
raise "Failed to refresh after #{max_attempts} attempts"
|
193
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
#
|
3
|
+
module Familia
|
4
|
+
# InstanceMethods - Module containing instance-level methods for Familia
|
5
|
+
#
|
6
|
+
# This module is included in classes that include Familia, providing
|
7
|
+
# instance-level functionality for Redis operations and object management.
|
8
|
+
#
|
9
|
+
class Horreum
|
10
|
+
|
11
|
+
# Settings - Module containing settings for Familia::Horreum (InstanceMethods)
|
12
|
+
#
|
13
|
+
module Settings
|
14
|
+
attr_writer :dump_method, :load_method, :suffix
|
15
|
+
|
16
|
+
def opts
|
17
|
+
@opts ||= {}
|
18
|
+
@opts
|
19
|
+
end
|
20
|
+
|
21
|
+
def redisdetails
|
22
|
+
{
|
23
|
+
uri: self.class.uri,
|
24
|
+
db: self.class.db,
|
25
|
+
key: rediskey,
|
26
|
+
type: redistype,
|
27
|
+
ttl: ttl,
|
28
|
+
realttl: realttl
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def ttl=(v)
|
33
|
+
@ttl = v.to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def ttl
|
37
|
+
@ttl || self.class.ttl
|
38
|
+
end
|
39
|
+
|
40
|
+
def db=(v)
|
41
|
+
@db = v.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def db
|
45
|
+
@db || self.class.db
|
46
|
+
end
|
47
|
+
|
48
|
+
def suffix
|
49
|
+
@suffix || self.class.suffix
|
50
|
+
end
|
51
|
+
|
52
|
+
def dump_method
|
53
|
+
@dump_method || self.class.dump_method
|
54
|
+
end
|
55
|
+
|
56
|
+
def load_method
|
57
|
+
@load_method || self.class.load_method
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
include Settings # these become Horreum instance methods
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
#
|
3
|
+
module Familia
|
4
|
+
# InstanceMethods - Module containing instance-level methods for Familia
|
5
|
+
#
|
6
|
+
# This module is included in classes that include Familia, providing
|
7
|
+
# instance-level functionality for Redis operations and object management.
|
8
|
+
#
|
9
|
+
class Horreum
|
10
|
+
|
11
|
+
# Utils - Module containing utility methods for Familia::Horreum (InstanceMethods)
|
12
|
+
#
|
13
|
+
module Utils
|
14
|
+
|
15
|
+
def redisuri(suffix = nil)
|
16
|
+
u = Familia.redisuri(self.class.uri) # returns URI::Redis
|
17
|
+
u.db ||= self.class.db.to_s # TODO: revisit logic (should the horrerum instance know its uri?)
|
18
|
+
u.key = rediskey(suffix)
|
19
|
+
u
|
20
|
+
end
|
21
|
+
|
22
|
+
# +suffix+ is the value to be used at the end of the redis key
|
23
|
+
# (e.g. `customer:customer_id:scores` would have `scores` as the suffix
|
24
|
+
# and `customer_id` would have been the identifier in that case).
|
25
|
+
#
|
26
|
+
# identifier is the value that distinguishes this object from others.
|
27
|
+
# Whether this is a Horreum or RedisType object, the value is taken
|
28
|
+
# from the `identifier` method).
|
29
|
+
#
|
30
|
+
def rediskey(suffix = nil, ignored = nil)
|
31
|
+
Familia.ld "[#rediskey] #{identifier} for #{self.class}"
|
32
|
+
raise Familia::NoIdentifier, "No identifier for #{self.class}" if identifier.to_s.empty?
|
33
|
+
suffix ||= self.suffix # use the instance method to get the default suffix
|
34
|
+
self.class.rediskey identifier, suffix
|
35
|
+
end
|
36
|
+
|
37
|
+
def join(*args)
|
38
|
+
Familia.join(args.map { |field| send(field) })
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
include Utils # these become Horreum instance methods
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,248 @@
|
|
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
|
+
# Automatically add a 'key' field if it's not already defined
|
98
|
+
# This ensures that every object has a unique identifier
|
99
|
+
unless self.class.fields.include?(:key)
|
100
|
+
# Define the 'key' field for this class
|
101
|
+
# This approach allows flexibility in how identifiers are generated
|
102
|
+
# while ensuring each object has a consistent way to be referenced
|
103
|
+
self.class.field :key # , default: -> { identifier }
|
104
|
+
end
|
105
|
+
|
106
|
+
# Implementing classes can define an init method to do any
|
107
|
+
# additional initialization. Notice that this is called
|
108
|
+
# after the fields are set.
|
109
|
+
init if respond_to?(:init)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Sets up related Redis objects for the instance
|
113
|
+
# This method is crucial for establishing Redis-based relationships
|
114
|
+
#
|
115
|
+
# This needs to be called in the initialize method.
|
116
|
+
#
|
117
|
+
def initialize_relatives
|
118
|
+
# Generate instances of each RedisType. These need to be
|
119
|
+
# unique for each instance of this class so they can piggyback
|
120
|
+
# on the specifc index of this instance.
|
121
|
+
#
|
122
|
+
# i.e.
|
123
|
+
# familia_object.rediskey == v1:bone:INDEXVALUE:object
|
124
|
+
# familia_object.redis_type.rediskey == v1:bone:INDEXVALUE:name
|
125
|
+
#
|
126
|
+
# See RedisType.install_redis_type
|
127
|
+
self.class.redis_types.each_pair do |name, redis_type_definition|
|
128
|
+
klass = redis_type_definition.klass
|
129
|
+
opts = redis_type_definition.opts
|
130
|
+
Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
|
131
|
+
|
132
|
+
# As a subclass of Familia::Horreum, we add ourselves as the parent
|
133
|
+
# automatically. This is what determines the rediskey for RedisType
|
134
|
+
# instance and which redis connection.
|
135
|
+
#
|
136
|
+
# e.g. If the parent's rediskey is `customer:customer_id:object`
|
137
|
+
# then the rediskey for this RedisType instance will be
|
138
|
+
# `customer:customer_id:name`.
|
139
|
+
#
|
140
|
+
opts[:parent] = self # unless opts.key(:parent)
|
141
|
+
|
142
|
+
# Instantiate the RedisType object and below we store it in
|
143
|
+
# an instance variable.
|
144
|
+
redis_type = klass.new name, opts
|
145
|
+
|
146
|
+
# Freezes the redis_type, making it immutable.
|
147
|
+
# This ensures the object's state remains consistent and prevents any modifications,
|
148
|
+
# safeguarding its integrity and making it thread-safe.
|
149
|
+
# Any attempts to change the object after this will raise a FrozenError.
|
150
|
+
redis_type.freeze
|
151
|
+
|
152
|
+
# e.g. customer.name #=> `#<Familia::HashKey:0x0000...>`
|
153
|
+
instance_variable_set :"@#{name}", redis_type
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Initializes the object with positional arguments.
|
158
|
+
# Maps each argument to a corresponding field in the order they are defined.
|
159
|
+
#
|
160
|
+
# @param args [Array] List of values to be assigned to fields
|
161
|
+
# @return [Array<Symbol>] List of field names that were successfully updated
|
162
|
+
# (i.e., had non-nil values assigned)
|
163
|
+
# @private
|
164
|
+
def initialize_with_positional_args(*args)
|
165
|
+
Familia.trace :INITIALIZE_ARGS, redis, args, caller(1..1) if Familia.debug?
|
166
|
+
self.class.fields.zip(args).filter_map do |field, value|
|
167
|
+
if value
|
168
|
+
send(:"#{field}=", value)
|
169
|
+
field.to_sym
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
private :initialize_with_positional_args
|
174
|
+
|
175
|
+
# Initializes the object with keyword arguments.
|
176
|
+
# Assigns values to fields based on the provided hash of field names and values.
|
177
|
+
# Handles both symbol and string keys to accommodate different sources of data.
|
178
|
+
#
|
179
|
+
# @param fields [Hash] Hash of field names (as symbols or strings) and their values
|
180
|
+
# @return [Array<Symbol>] List of field names that were successfully updated
|
181
|
+
# (i.e., had non-nil values assigned)
|
182
|
+
# @private
|
183
|
+
def initialize_with_keyword_args(**fields)
|
184
|
+
Familia.trace :INITIALIZE_KWARGS, redis, fields.keys, caller(1..1) if Familia.debug?
|
185
|
+
self.class.fields.filter_map do |field|
|
186
|
+
# Redis will give us field names as strings back, but internally
|
187
|
+
# we use symbols. So we check for both.
|
188
|
+
value = fields[field.to_sym] || fields[field.to_s]
|
189
|
+
if value
|
190
|
+
send(:"#{field}=", value)
|
191
|
+
field.to_sym
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
private :initialize_with_keyword_args
|
196
|
+
|
197
|
+
# A thin wrapper around the private initialize method that accepts a field
|
198
|
+
# hash and refreshes the existing object.
|
199
|
+
#
|
200
|
+
# This method is part of horreum.rb rather than serialization.rb because it
|
201
|
+
# operates solely on the provided values and doesn't query Redis or other
|
202
|
+
# external sources. That's why it's called "optimistic" refresh: it assumes
|
203
|
+
# the provided values are correct and updates the object accordingly.
|
204
|
+
#
|
205
|
+
# @see #refresh!
|
206
|
+
#
|
207
|
+
# @param fields [Hash] A hash of field names and their new values to update
|
208
|
+
# the object with.
|
209
|
+
# @return [Array] The list of field names that were updated.
|
210
|
+
def optimistic_refresh(**fields)
|
211
|
+
Familia.ld "[optimistic_refresh] #{self.class} #{rediskey} #{fields.keys}"
|
212
|
+
initialize_with_keyword_args(**fields)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Determines the unique identifier for the instance
|
216
|
+
# This method is used to generate Redis keys for the object
|
217
|
+
def identifier
|
218
|
+
definition = self.class.identifier # e.g.
|
219
|
+
# When definition is a symbol or string, assume it's an instance method
|
220
|
+
# to call on the object to get the unique identifier. When it's a callable
|
221
|
+
# object, call it with the object as the argument. When it's an array,
|
222
|
+
# call each method in turn and join the results. When it's nil, raise
|
223
|
+
# an error
|
224
|
+
unique_id = case definition
|
225
|
+
when Symbol, String
|
226
|
+
send(definition)
|
227
|
+
when Proc
|
228
|
+
definition.call(self)
|
229
|
+
when Array
|
230
|
+
Familia.join(definition.map { |method| send(method) })
|
231
|
+
else
|
232
|
+
raise Problem, "Invalid identifier definition: #{definition.inspect}"
|
233
|
+
end
|
234
|
+
|
235
|
+
# If the unique_id is nil, raise an error
|
236
|
+
raise Problem, "Identifier is nil for #{self.class}" if unique_id.nil?
|
237
|
+
raise Problem, 'Identifier is empty' if unique_id.empty?
|
238
|
+
|
239
|
+
unique_id
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
require_relative 'horreum/class_methods'
|
245
|
+
require_relative 'horreum/commands'
|
246
|
+
require_relative 'horreum/serialization'
|
247
|
+
require_relative 'horreum/settings'
|
248
|
+
require_relative 'horreum/utils'
|