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,240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'relations_management'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
class Horreum
|
7
|
+
# Class-level instance variables
|
8
|
+
# These are set up as nil initially and populated later
|
9
|
+
@redis = nil
|
10
|
+
@identifier = nil
|
11
|
+
@fields = nil # []
|
12
|
+
@ttl = nil
|
13
|
+
@db = nil
|
14
|
+
@uri = nil
|
15
|
+
@suffix = nil
|
16
|
+
@prefix = nil
|
17
|
+
@class_redis_types = nil # {}
|
18
|
+
@redis_types = nil # {}
|
19
|
+
@defined_fields = nil # {}
|
20
|
+
@dump_method = nil
|
21
|
+
@load_method = nil
|
22
|
+
|
23
|
+
# ClassMethods: Provides class-level functionality for Horreum
|
24
|
+
#
|
25
|
+
# This module is extended into classes that include Familia::Horreum,
|
26
|
+
# providing methods for Redis operations and object management.
|
27
|
+
#
|
28
|
+
# Key features:
|
29
|
+
# * Includes RelationsManagement for Redis-type field handling
|
30
|
+
# * Defines methods for managing fields, identifiers, and Redis keys
|
31
|
+
# * Provides utility methods for working with Redis objects
|
32
|
+
#
|
33
|
+
module ClassMethods
|
34
|
+
include Familia::Settings
|
35
|
+
include Familia::Horreum::RelationsManagement
|
36
|
+
|
37
|
+
attr_accessor :parent
|
38
|
+
attr_writer :redis, :dump_method, :load_method
|
39
|
+
|
40
|
+
def redis
|
41
|
+
@redis || Familia.redis(uri || db)
|
42
|
+
end
|
43
|
+
|
44
|
+
# The object field or instance method to call to get the unique identifier
|
45
|
+
# for that instance. The value returned by this method will be used to
|
46
|
+
# generate the key for the object in Redis.
|
47
|
+
def identifier(val = nil)
|
48
|
+
@identifier = val if val
|
49
|
+
@identifier
|
50
|
+
end
|
51
|
+
|
52
|
+
# Define a field for the class. This will create getter and setter
|
53
|
+
# instance methods just like any "attr_accessor" methods.
|
54
|
+
def field(name)
|
55
|
+
fields << name
|
56
|
+
attr_accessor name
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the list of field names defined for the class in the order
|
60
|
+
# that they were defined. i.e. `field :a; field :b; fields => [:a, :b]`.
|
61
|
+
def fields
|
62
|
+
@fields ||= []
|
63
|
+
@fields
|
64
|
+
end
|
65
|
+
|
66
|
+
def class_redis_types
|
67
|
+
@class_redis_types ||= {}
|
68
|
+
@class_redis_types
|
69
|
+
end
|
70
|
+
|
71
|
+
def class_redis_types?(name)
|
72
|
+
class_redis_types.key? name.to_s.to_sym
|
73
|
+
end
|
74
|
+
|
75
|
+
def redis_object?(name)
|
76
|
+
redis_types.key? name.to_s.to_sym
|
77
|
+
end
|
78
|
+
|
79
|
+
def redis_types
|
80
|
+
@redis_types ||= {}
|
81
|
+
@redis_types
|
82
|
+
end
|
83
|
+
|
84
|
+
def defined_fields
|
85
|
+
@defined_fields ||= {}
|
86
|
+
@defined_fields
|
87
|
+
end
|
88
|
+
|
89
|
+
def ttl(v = nil)
|
90
|
+
@ttl = v unless v.nil?
|
91
|
+
@ttl || parent&.ttl
|
92
|
+
end
|
93
|
+
|
94
|
+
def db(v = nil)
|
95
|
+
@db = v unless v.nil?
|
96
|
+
@db || parent&.db
|
97
|
+
end
|
98
|
+
|
99
|
+
def uri(v = nil)
|
100
|
+
@uri = v unless v.nil?
|
101
|
+
@uri || parent&.uri
|
102
|
+
end
|
103
|
+
|
104
|
+
def all(suffix = :object)
|
105
|
+
# objects that could not be parsed will be nil
|
106
|
+
keys(suffix).filter_map { |k| from_key(k) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def any?(filter = '*')
|
110
|
+
size(filter) > 0
|
111
|
+
end
|
112
|
+
|
113
|
+
def size(filter = '*')
|
114
|
+
redis.keys(rediskey(filter)).compact.size
|
115
|
+
end
|
116
|
+
|
117
|
+
def suffix(a = nil, &blk)
|
118
|
+
@suffix = a || blk if a || !blk.nil?
|
119
|
+
@suffix || Familia.default_suffix
|
120
|
+
end
|
121
|
+
|
122
|
+
def prefix(a = nil)
|
123
|
+
@prefix = a if a
|
124
|
+
@prefix || name.downcase.gsub('::', Familia.delim).to_sym
|
125
|
+
end
|
126
|
+
|
127
|
+
def create *args
|
128
|
+
me = from_array(*args)
|
129
|
+
raise "#{self} exists: #{me.rediskey}" if me.exists?
|
130
|
+
|
131
|
+
me.save
|
132
|
+
me
|
133
|
+
end
|
134
|
+
|
135
|
+
def multiget(*ids)
|
136
|
+
ids = rawmultiget(*ids)
|
137
|
+
ids.filter_map { |json| from_json(json) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def rawmultiget(*ids)
|
141
|
+
ids.collect! { |objid| rediskey(objid) }
|
142
|
+
return [] if ids.compact.empty?
|
143
|
+
|
144
|
+
Familia.trace :MULTIGET, redis, "#{ids.size}: #{ids}", caller if Familia.debug?
|
145
|
+
redis.mget(*ids)
|
146
|
+
end
|
147
|
+
|
148
|
+
def from_key(objkey)
|
149
|
+
raise ArgumentError, 'Empty key' if objkey.to_s.empty?
|
150
|
+
|
151
|
+
# We use a lower-level method here b/c we're working with the
|
152
|
+
# full key and not just the identifier.
|
153
|
+
does_exist = redis.exists(objkey).positive?
|
154
|
+
|
155
|
+
Familia.ld "[.from_key] #{self} from key #{objkey} (exists: #{does_exist})"
|
156
|
+
Familia.trace :LOAD, redis, objkey, caller if Familia.debug?
|
157
|
+
|
158
|
+
# This is reason for calling exists first. We want to definitively and without any
|
159
|
+
# ambiguity know if the object exists in Redis. If it doesn't, we return nil. If
|
160
|
+
# it does, we proceed to load the object. Otherwise, hgetall will return an empty
|
161
|
+
# hash, which will be passed to the constructor, which will then be annoying to
|
162
|
+
# debug.
|
163
|
+
return unless does_exist
|
164
|
+
|
165
|
+
obj = redis.hgetall(objkey) # horreum objects are persisted as redis hashes
|
166
|
+
Familia.trace :HGETALL, redis, "#{objkey}: #{obj.inspect}", caller if Familia.debug?
|
167
|
+
|
168
|
+
new(**obj)
|
169
|
+
end
|
170
|
+
|
171
|
+
def from_redis(identifier, suffix = :object)
|
172
|
+
return nil if identifier.to_s.empty?
|
173
|
+
|
174
|
+
objkey = rediskey(identifier, suffix)
|
175
|
+
Familia.ld "[.from_redis] #{self} from key #{objkey})"
|
176
|
+
Familia.trace :FROMREDIS, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
|
177
|
+
from_key objkey
|
178
|
+
end
|
179
|
+
|
180
|
+
def exists?(identifier, suffix = :object)
|
181
|
+
return false if identifier.to_s.empty?
|
182
|
+
|
183
|
+
objkey = rediskey identifier, suffix
|
184
|
+
|
185
|
+
ret = redis.exists objkey
|
186
|
+
if Familia.debug?
|
187
|
+
Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}",
|
188
|
+
caller
|
189
|
+
end
|
190
|
+
ret.positive?
|
191
|
+
end
|
192
|
+
|
193
|
+
def destroy!(identifier, suffix = :object)
|
194
|
+
return false if identifier.to_s.empty?
|
195
|
+
|
196
|
+
objkey = rediskey identifier, suffix
|
197
|
+
|
198
|
+
ret = redis.del objkey
|
199
|
+
if Familia.debug?
|
200
|
+
Familia.trace :DELETED, redis, "#{objkey}: #{ret.inspect}",
|
201
|
+
caller
|
202
|
+
end
|
203
|
+
ret.positive?
|
204
|
+
end
|
205
|
+
|
206
|
+
def find(suffix = '*')
|
207
|
+
redis.keys(rediskey('*', suffix)) || []
|
208
|
+
end
|
209
|
+
|
210
|
+
def qstamp(quantum = nil, pattern = nil, now = Familia.now)
|
211
|
+
quantum ||= ttl || 10.minutes
|
212
|
+
pattern ||= '%H%M'
|
213
|
+
rounded = now - (now % quantum)
|
214
|
+
Time.at(rounded).utc.strftime(pattern)
|
215
|
+
end
|
216
|
+
|
217
|
+
# +identifier+ can be a value or an Array of values used to create the index.
|
218
|
+
# We don't enforce a default suffix; that's left up to the instance.
|
219
|
+
# The suffix is used to differentiate between different types of objects.
|
220
|
+
#
|
221
|
+
#
|
222
|
+
# A nil +suffix+ will not be included in the key.
|
223
|
+
def rediskey(identifier, suffix = self.suffix)
|
224
|
+
Familia.ld "[.rediskey] #{identifier} for #{self} (suffix:#{suffix})"
|
225
|
+
raise NoIdentifier, self if identifier.to_s.empty?
|
226
|
+
|
227
|
+
identifier &&= identifier.to_s
|
228
|
+
Familia.rediskey(prefix, identifier, suffix)
|
229
|
+
end
|
230
|
+
|
231
|
+
def dump_method
|
232
|
+
@dump_method || :to_json # Familia.dump_method
|
233
|
+
end
|
234
|
+
|
235
|
+
def load_method
|
236
|
+
@load_method || :from_json # Familia.load_method
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,59 @@
|
|
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 Redis commands (InstanceMethods)
|
12
|
+
#
|
13
|
+
# NOTE: There is no hgetall for Horreum. This is because Horreum
|
14
|
+
# is a single hash in Redis that we aren't meant to have be working
|
15
|
+
# on in memory for more than, making changes -> committing. To
|
16
|
+
# emphasize this, instead of "refreshing" the object with hgetall,
|
17
|
+
# just load the object again.
|
18
|
+
#
|
19
|
+
module Commands
|
20
|
+
|
21
|
+
def exists?
|
22
|
+
ret = redis.exists rediskey
|
23
|
+
ret.positive?
|
24
|
+
end
|
25
|
+
|
26
|
+
def expire(ttl = nil)
|
27
|
+
ttl ||= self.class.ttl
|
28
|
+
redis.expire rediskey, ttl.to_i
|
29
|
+
end
|
30
|
+
|
31
|
+
def realttl
|
32
|
+
redis.ttl rediskey
|
33
|
+
end
|
34
|
+
|
35
|
+
def hdel!(field)
|
36
|
+
redis.hdel rediskey, field
|
37
|
+
end
|
38
|
+
|
39
|
+
def redistype(suffix = nil)
|
40
|
+
redis.type rediskey(suffix)
|
41
|
+
end
|
42
|
+
|
43
|
+
def hmset(suffix = nil)
|
44
|
+
suffix ||= self.class.suffix
|
45
|
+
redis.hmset rediskey(suffix), to_h
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete!
|
49
|
+
Familia.trace :DELETE!, redis, redisuri, caller(1..1) if Familia.debug?
|
50
|
+
ret = redis.del rediskey
|
51
|
+
ret.positive?
|
52
|
+
end
|
53
|
+
protected :delete!
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
include Commands # these become Familia::Horreum instance methods
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Familia
|
2
|
+
class Horreum
|
3
|
+
#
|
4
|
+
# RelationsManagement: Manages Redis-type fields and relations
|
5
|
+
#
|
6
|
+
# This module uses metaprogramming to dynamically create methods
|
7
|
+
# for managing different types of Redis objects (e.g., sets, lists, hashes).
|
8
|
+
#
|
9
|
+
# Key metaprogramming features:
|
10
|
+
# * Dynamically defines methods for each Redis type (e.g., set, list, hashkey)
|
11
|
+
# * Creates both instance-level and class-level relation methods
|
12
|
+
# * Provides query methods for checking relation types
|
13
|
+
#
|
14
|
+
# Usage:
|
15
|
+
# Include this module in classes that need Redis-type management
|
16
|
+
# Call setup_relations_accessors to initialize the feature
|
17
|
+
#
|
18
|
+
module RelationsManagement
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
base.setup_relations_accessors
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
# Sets up all Redis-type related methods
|
26
|
+
# This method is the core of the metaprogramming logic
|
27
|
+
#
|
28
|
+
def setup_relations_accessors
|
29
|
+
Familia::RedisType.registered_types.each_pair do |kind, klass|
|
30
|
+
Familia.ld "[registered_types] #{kind} => #{klass}"
|
31
|
+
|
32
|
+
# Dynamically define instance-level relation methods
|
33
|
+
#
|
34
|
+
# Once defined, these methods can be used at the class-level of a
|
35
|
+
# Familia member to define *instance-level* relations to any of the
|
36
|
+
# RedisType types (e.g. set, list, hash, etc).
|
37
|
+
#
|
38
|
+
define_method :"#{kind}" do |*args|
|
39
|
+
name, opts = *args
|
40
|
+
attach_instance_redis_object_relation name, klass, opts
|
41
|
+
redis_types[name.to_s.to_sym]
|
42
|
+
end
|
43
|
+
define_method :"#{kind}?" do |name|
|
44
|
+
obj = redis_types[name.to_s.to_sym]
|
45
|
+
!obj.nil? && klass == obj.klass
|
46
|
+
end
|
47
|
+
define_method :"#{kind}s" do
|
48
|
+
names = redis_types.keys.select { |name| send(:"#{kind}?", name) }
|
49
|
+
names.collect! { |name| redis_types[name] }
|
50
|
+
names
|
51
|
+
end
|
52
|
+
|
53
|
+
# Dynamically define class-level relation methods
|
54
|
+
#
|
55
|
+
# Once defined, these methods can be used at the class-level of a
|
56
|
+
# Familia member to define *class-level relations* to any of the
|
57
|
+
# RedisType types (e.g. class_set, class_list, class_hash, etc).
|
58
|
+
#
|
59
|
+
define_method :"class_#{kind}" do |*args|
|
60
|
+
name, opts = *args
|
61
|
+
attach_class_redis_object_relation name, klass, opts
|
62
|
+
end
|
63
|
+
define_method :"class_#{kind}?" do |name|
|
64
|
+
obj = class_redis_types[name.to_s.to_sym]
|
65
|
+
!obj.nil? && klass == obj.klass
|
66
|
+
end
|
67
|
+
define_method :"class_#{kind}s" do
|
68
|
+
names = class_redis_types.keys.select { |name| send(:"class_#{kind}?", name) }
|
69
|
+
# TODO: This returns instances of the RedisType class which
|
70
|
+
# also contain the options. This is different from the instance
|
71
|
+
# RedisTypes defined above which returns the Struct of name, klass, and opts.
|
72
|
+
# names.collect! { |name| self.send name }
|
73
|
+
# OR NOT:
|
74
|
+
names.collect! { |name| class_redis_types[name] }
|
75
|
+
names
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# End of ClassMethods module
|
81
|
+
|
82
|
+
# Creates an instance-level relation
|
83
|
+
def attach_instance_redis_object_relation(name, klass, opts)
|
84
|
+
Familia.ld "[Attaching instance-level #{name}] #{klass} => (#{self}) #{opts}"
|
85
|
+
raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
|
86
|
+
|
87
|
+
name = name.to_s.to_sym
|
88
|
+
opts ||= {}
|
89
|
+
|
90
|
+
redis_types[name] = Struct.new(:name, :klass, :opts).new
|
91
|
+
redis_types[name].name = name
|
92
|
+
redis_types[name].klass = klass
|
93
|
+
redis_types[name].opts = opts
|
94
|
+
|
95
|
+
attr_reader name
|
96
|
+
|
97
|
+
define_method :"#{name}=" do |val|
|
98
|
+
send(name).replace val
|
99
|
+
end
|
100
|
+
define_method :"#{name}?" do
|
101
|
+
!send(name).empty?
|
102
|
+
end
|
103
|
+
|
104
|
+
redis_types[name]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Creates a class-level relation
|
108
|
+
def attach_class_redis_object_relation(name, klass, opts)
|
109
|
+
Familia.ld "[#{self}] Attaching class-level #{name} #{klass} => #{opts}"
|
110
|
+
raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
|
111
|
+
|
112
|
+
name = name.to_s.to_sym
|
113
|
+
opts = opts.nil? ? {} : opts.clone
|
114
|
+
opts[:parent] = self unless opts.key?(:parent)
|
115
|
+
|
116
|
+
class_redis_types[name] = Struct.new(:name, :klass, :opts).new
|
117
|
+
class_redis_types[name].name = name
|
118
|
+
class_redis_types[name].klass = klass
|
119
|
+
class_redis_types[name].opts = opts
|
120
|
+
|
121
|
+
# An accessor method created in the metaclass will
|
122
|
+
# access the instance variables for this class.
|
123
|
+
singleton_class.attr_reader name
|
124
|
+
|
125
|
+
define_singleton_method :"#{name}=" do |v|
|
126
|
+
send(name).replace v
|
127
|
+
end
|
128
|
+
define_singleton_method :"#{name}?" do
|
129
|
+
!send(name).empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
redis_object = klass.new name, opts
|
133
|
+
redis_object.freeze
|
134
|
+
instance_variable_set(:"@#{name}", redis_object)
|
135
|
+
|
136
|
+
class_redis_types[name]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
# End of RelationsManagement module
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,154 @@
|
|
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
|
+
module Serialization
|
14
|
+
#include Familia::RedisType::Serialization
|
15
|
+
|
16
|
+
attr_writer :redis
|
17
|
+
|
18
|
+
def redis
|
19
|
+
@redis || self.class.redis
|
20
|
+
end
|
21
|
+
|
22
|
+
def transaction
|
23
|
+
original_redis = self.redis
|
24
|
+
|
25
|
+
begin
|
26
|
+
redis.multi do |conn|
|
27
|
+
self.instance_variable_set(:@redis, conn)
|
28
|
+
yield(conn)
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
self.redis = original_redis
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# A thin wrapper around `commit_fields` that updates the timestamps and
|
36
|
+
# returns a boolean.
|
37
|
+
def save
|
38
|
+
Familia.trace :SAVE, redis, redisuri, caller(1..1) if Familia.debug?
|
39
|
+
|
40
|
+
# Update timestamp fields
|
41
|
+
self.updated = Familia.now.to_i
|
42
|
+
self.created = Familia.now.to_i unless self.created
|
43
|
+
|
44
|
+
# Thr return value of commit_fields is an array of strings: ["OK"].
|
45
|
+
ret = commit_fields # e.g. ["OK"]
|
46
|
+
|
47
|
+
Familia.ld "[save] #{self.class} #{rediskey} #{ret}"
|
48
|
+
|
49
|
+
# Convert the return value to a boolean
|
50
|
+
ret.all? { |value| value == "OK" }
|
51
|
+
end
|
52
|
+
|
53
|
+
# +return: [Array<String>] The return value of the Redis multi command
|
54
|
+
def commit_fields
|
55
|
+
Familia.ld "[commit_fields] #{self.class} #{rediskey} #{to_h}"
|
56
|
+
transaction do |conn|
|
57
|
+
hmset
|
58
|
+
update_expiration
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def destroy!
|
63
|
+
Familia.trace :DESTROY, redis, redisuri, caller(1..1) if Familia.debug?
|
64
|
+
delete!
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_h
|
68
|
+
# Use self.class.fields to efficiently generate a hash
|
69
|
+
# of all the fields for this object
|
70
|
+
self.class.fields.inject({}) do |hsh, field|
|
71
|
+
val = send(field)
|
72
|
+
prepared = val.to_s
|
73
|
+
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared.class}"
|
74
|
+
hsh[field] = prepared
|
75
|
+
hsh
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_a
|
80
|
+
self.class.fields.map do |field|
|
81
|
+
val = send(field)
|
82
|
+
Familia.ld " [to_a] field: #{field} val: #{val}"
|
83
|
+
to_redis(val)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# The to_redis method in Familia::Redistype and Familia::Horreum serve similar purposes
|
88
|
+
# but have some key differences in their implementation:
|
89
|
+
#
|
90
|
+
# Similarities:
|
91
|
+
# - Both methods aim to serialize various data types for Redis storage
|
92
|
+
# - Both handle basic data types like String, Symbol, and Numeric
|
93
|
+
# - Both have provisions for custom serialization methods
|
94
|
+
#
|
95
|
+
# Differences:
|
96
|
+
# - Familia::Redistype uses the opts[:class] for type hints
|
97
|
+
# - Familia::Horreum had more explicit type checking and conversion
|
98
|
+
# - Familia::Redistype includes more extensive debug tracing
|
99
|
+
#
|
100
|
+
# The centralized Familia.distinguisher method accommodates both approaches by:
|
101
|
+
# 1. Handling a wide range of data types, including those from both implementations
|
102
|
+
# 2. Providing a 'strict_values' option for flexible type handling
|
103
|
+
# 3. Supporting custom serialization through a dump_method
|
104
|
+
# 4. Including debug tracing similar to Familia::Redistype
|
105
|
+
#
|
106
|
+
# By using Familia.distinguisher, we achieve more consistent behavior across
|
107
|
+
# different parts of the library while maintaining the flexibility to handle
|
108
|
+
# various data types and custom serialization needs. This centralization
|
109
|
+
# also makes it easier to extend or modify serialization behavior in the future.
|
110
|
+
#
|
111
|
+
def to_redis(val)
|
112
|
+
prepared = Familia.distinguisher(val, false)
|
113
|
+
|
114
|
+
if prepared.nil? && val.respond_to?(dump_method)
|
115
|
+
prepared = val.send(dump_method)
|
116
|
+
end
|
117
|
+
|
118
|
+
Familia.ld "[#{self.class}#to_redis] nil returned for #{self.class}##{name}" if prepared.nil?
|
119
|
+
prepared
|
120
|
+
end
|
121
|
+
|
122
|
+
def update_expiration(ttl = nil)
|
123
|
+
ttl ||= opts[:ttl]
|
124
|
+
return if ttl.to_i.zero? # nil will be zero
|
125
|
+
|
126
|
+
Familia.ld "#{rediskey} to #{ttl}"
|
127
|
+
expire ttl.to_i
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
include Serialization # these become Horreum instance methods
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
__END__
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
# From RedisHash
|
140
|
+
def save
|
141
|
+
hsh = { :key => identifier }
|
142
|
+
ret = commit_fields hsh
|
143
|
+
ret == "OK"
|
144
|
+
end
|
145
|
+
|
146
|
+
def update_fields hsh={}
|
147
|
+
check_identifier!
|
148
|
+
hsh[:updated] = OT.now.to_i
|
149
|
+
hsh[:created] = OT.now.to_i unless has_key?(:created)
|
150
|
+
ret = update hsh # update is defined in HashKey
|
151
|
+
## NOTE: caching here like this only works if hsh has all keys
|
152
|
+
#self.cache.replace hsh
|
153
|
+
ret
|
154
|
+
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,43 @@
|
|
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 = self.suffix, 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
|
+
self.class.rediskey identifier, suffix
|
34
|
+
end
|
35
|
+
|
36
|
+
def join(*args)
|
37
|
+
Familia.join(args.map { |field| send(field) })
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
include Utils # these become Horreum instance methods
|
42
|
+
end
|
43
|
+
end
|