familia 1.0.0.pre.rc1 → 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/Gemfile.lock +1 -1
- data/README.md +65 -12
- data/VERSION.yml +1 -1
- data/lib/familia/features/safe_dump.rb +21 -2
- data/lib/familia/horreum/class_methods.rb +71 -19
- data/lib/familia/horreum/commands.rb +51 -4
- data/lib/familia/horreum/serialization.rb +69 -30
- data/lib/familia/horreum/utils.rb +2 -1
- data/lib/familia/horreum.rb +58 -8
- data/lib/familia/logging.rb +4 -21
- data/lib/familia/redistype.rb +10 -10
- data/lib/familia/refinements.rb +88 -0
- data/lib/familia/types/hashkey.rb +2 -3
- data/lib/familia/utils.rb +2 -6
- data/lib/familia.rb +1 -0
- data/try/27_redis_horreum_try.rb +53 -0
- data/try/test_helpers.rb +9 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4f3c02cad65603669b086ec26f31512004e939da510fec97fcc3803f132e5f5
|
4
|
+
data.tar.gz: cdd710ea75ff3c6ce3ccb88caef97226ee9fb549435fce605433621a8583f06d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '058e49695798298238523d571a4d21b47234874b47af018fdb085d22a430fa8db2ca725cb3103e0c8eee309b000eea7598cc4e9aed00373191d393af8d4ab27d'
|
7
|
+
data.tar.gz: 9ff11c6c341d540e69ddb9d0518b67c9463b2248899b6f74a94a92b609140ed064e303e3559a455847727d5f7581aa41fccce5fdaac5effb5d74a30202e75721
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,31 +1,84 @@
|
|
1
|
-
# Familia - 1.0.0-
|
1
|
+
# Familia - 1.0.0-rc2 (August 2024)
|
2
2
|
|
3
3
|
**Organize and store ruby objects in Redis. A Ruby ORM for Redis.**
|
4
4
|
|
5
|
+
Familia provides a powerful and flexible way to interact with Redis using Ruby objects. It's designed to make working with Redis as natural as working with Ruby classes.
|
6
|
+
|
5
7
|
## Installation
|
6
8
|
|
7
9
|
Get it in one of the following ways:
|
8
10
|
|
9
|
-
* In your Gemfile: `gem 'familia', '>= 1.0.0-
|
11
|
+
* In your Gemfile: `gem 'familia', '>= 1.0.0-rc2'`
|
10
12
|
* Install it by hand: `gem install familia`
|
11
13
|
* Or for development: `git clone git@github.com:delano/familia.git`
|
12
14
|
|
13
15
|
## Basic Example
|
14
16
|
|
15
17
|
```ruby
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
18
|
+
class Flower < Familia::Horreum
|
19
|
+
identifier :generate_id
|
20
|
+
field :token
|
21
|
+
field :name
|
22
|
+
list :owners
|
23
|
+
set :tags
|
24
|
+
zset :metrics
|
25
|
+
hashkey :props
|
26
|
+
string :counter
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
## What Familia::Horreum Can Do
|
31
|
+
|
32
|
+
Familia::Horreum provides a powerful abstraction layer over Redis, allowing you to:
|
33
|
+
|
34
|
+
1. **Define Redis-backed Ruby Classes**: As shown in the example, you can easily define classes that map to Redis structures.
|
35
|
+
|
36
|
+
2. **Use Various Redis Data Types**: Familia supports multiple Redis data types:
|
37
|
+
- `field`: For simple key-value pairs
|
38
|
+
- `list`: For Redis lists
|
39
|
+
- `set`: For Redis sets
|
40
|
+
- `zset`: For Redis sorted sets
|
41
|
+
- `hashkey`: For Redis hashes
|
42
|
+
- `string`: For Redis strings
|
43
|
+
|
44
|
+
3. **Custom Identifiers**: Use the `identifier` method to specify how objects are uniquely identified in Redis.
|
45
|
+
|
46
|
+
4. **Automatic Serialization**: Familia handles the serialization and deserialization of your objects to and from Redis.
|
47
|
+
|
48
|
+
5. **Redis Commands as Ruby Methods**: Interact with Redis using familiar Ruby syntax instead of raw Redis commands.
|
49
|
+
|
50
|
+
6. **TTL Support**: Set expiration times for your objects in Redis.
|
51
|
+
|
52
|
+
7. **Flexible Configuration**: Configure Redis connection details, serialization methods, and more.
|
53
|
+
|
54
|
+
## Advanced Features
|
55
|
+
|
56
|
+
- **API Versioning**: Familia supports API versioning to help manage changes in your data model over time.
|
57
|
+
- **Custom Serialization**: You can specify custom serialization methods for your objects.
|
58
|
+
- **Redis URI Support**: Easily connect to Redis using URI strings.
|
59
|
+
- **Debugging Tools**: Built-in debugging capabilities to help troubleshoot Redis interactions.
|
60
|
+
|
61
|
+
## Usage Example
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
# Create a new Flower
|
65
|
+
rose = Flower.new
|
66
|
+
rose.name = "Red Rose"
|
67
|
+
rose.tags << "romantic" << "red"
|
68
|
+
rose.owners.push("Alice", "Bob")
|
69
|
+
rose.save
|
70
|
+
|
71
|
+
# Retrieve a Flower
|
72
|
+
retrieved_rose = Flower.get(rose.identifier)
|
73
|
+
puts retrieved_rose.name # => "Red Rose"
|
74
|
+
puts retrieved_rose.tags.members # => ["romantic", "red"]
|
26
75
|
```
|
27
76
|
|
28
77
|
## More Information
|
29
78
|
|
30
79
|
* [Github](https://github.com/delano/familia)
|
31
80
|
* [Rubygems](https://rubygems.org/gems/familia)
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
data/VERSION.yml
CHANGED
@@ -12,13 +12,15 @@ module Familia::Features
|
|
12
12
|
# a symbol, the method with the same name will be called on the object to
|
13
13
|
# retrieve the value. If the field is a hash, the key is the field name and
|
14
14
|
# the value is a lambda that will be called with the object as an argument.
|
15
|
-
#
|
15
|
+
# The hash syntax allows you to:
|
16
16
|
# * define a field name that is different from the method name
|
17
17
|
# * define a field that requires some computation on-the-fly
|
18
18
|
# * define a field that is not a method on the object
|
19
19
|
#
|
20
20
|
# Example:
|
21
21
|
#
|
22
|
+
# feature :safe_dump
|
23
|
+
#
|
22
24
|
# @safe_dump_fields = [
|
23
25
|
# :objid,
|
24
26
|
# :updated,
|
@@ -26,11 +28,26 @@ module Familia::Features
|
|
26
28
|
# { :active => ->(obj) { obj.active? } }
|
27
29
|
# ]
|
28
30
|
#
|
29
|
-
# Internally, all fields are normalized to the hash syntax and
|
31
|
+
# Internally, all fields are normalized to the hash syntax and stored in
|
30
32
|
# @safe_dump_field_map. `SafeDump.safe_dump_fields` returns only the list
|
31
33
|
# of symbols in the order they were defined. From the example above, it would
|
32
34
|
# return `[:objid, :updated, :created, :active]`.
|
33
35
|
#
|
36
|
+
# Standalone Usage:
|
37
|
+
#
|
38
|
+
# You can also use SafeDump by including it in your model and defining the
|
39
|
+
# safe dump fields using the class instance variable `@safe_dump_fields`.
|
40
|
+
#
|
41
|
+
# Example:
|
42
|
+
#
|
43
|
+
# class MyModel
|
44
|
+
# include Familia::Features::SafeDump
|
45
|
+
#
|
46
|
+
# @safe_dump_fields = [
|
47
|
+
# :id, :name, { active: ->(obj) { obj.active? } }
|
48
|
+
# ]
|
49
|
+
# end
|
50
|
+
#
|
34
51
|
module SafeDump
|
35
52
|
@dump_method = :to_json
|
36
53
|
@load_method = :from_json
|
@@ -148,6 +165,8 @@ end
|
|
148
165
|
|
149
166
|
__END__
|
150
167
|
|
168
|
+
# Some leftovers related to dump_method and load_method
|
169
|
+
|
151
170
|
if value_to_distunguish.is_a?(Familia::Horreum)
|
152
171
|
Familia.trace :DISTINGUISHER, redis, "horreum", caller(1..1) if Familia.debug?
|
153
172
|
value_to_distunguish.identifier
|
@@ -8,15 +8,14 @@ module Familia
|
|
8
8
|
# These are set up as nil initially and populated later
|
9
9
|
@redis = nil
|
10
10
|
@identifier = nil
|
11
|
-
@fields = nil # []
|
12
11
|
@ttl = nil
|
13
12
|
@db = nil
|
14
13
|
@uri = nil
|
15
14
|
@suffix = nil
|
16
15
|
@prefix = nil
|
16
|
+
@fields = nil # []
|
17
17
|
@class_redis_types = nil # {}
|
18
18
|
@redis_types = nil # {}
|
19
|
-
@defined_fields = nil # {}
|
20
19
|
@dump_method = nil
|
21
20
|
@load_method = nil
|
22
21
|
|
@@ -54,6 +53,19 @@ module Familia
|
|
54
53
|
def field(name)
|
55
54
|
fields << name
|
56
55
|
attr_accessor name
|
56
|
+
|
57
|
+
# Every field gets a fast writer method for immediately persisting
|
58
|
+
fast_writer! name
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return The return value from redis client for hset command
|
62
|
+
def fast_writer!(name)
|
63
|
+
define_method :"#{name}!" do |value|
|
64
|
+
prepared = to_redis(value)
|
65
|
+
Familia.ld "[.fast_writer!] #{name} val: #{value.class} prepared: #{prepared.class}"
|
66
|
+
send :"#{name}=", value # use the existing accessor
|
67
|
+
hset name, prepared # persist to Redis without delay
|
68
|
+
end
|
57
69
|
end
|
58
70
|
|
59
71
|
# Returns the list of field names defined for the class in the order
|
@@ -81,11 +93,6 @@ module Familia
|
|
81
93
|
@redis_types
|
82
94
|
end
|
83
95
|
|
84
|
-
def defined_fields
|
85
|
-
@defined_fields ||= {}
|
86
|
-
@defined_fields
|
87
|
-
end
|
88
|
-
|
89
96
|
def ttl(v = nil)
|
90
97
|
@ttl = v unless v.nil?
|
91
98
|
@ttl || parent&.ttl
|
@@ -145,6 +152,34 @@ module Familia
|
|
145
152
|
redis.mget(*ids)
|
146
153
|
end
|
147
154
|
|
155
|
+
# Retrieves and instantiates an object from Redis using the full object
|
156
|
+
# key.
|
157
|
+
#
|
158
|
+
# @param objkey [String] The full Redis key for the object.
|
159
|
+
# @return [Object, nil] An instance of the class if the key exists, nil
|
160
|
+
# otherwise.
|
161
|
+
# @raise [ArgumentError] If the provided key is empty.
|
162
|
+
#
|
163
|
+
# This method performs a two-step process to safely retrieve and
|
164
|
+
# instantiate objects:
|
165
|
+
#
|
166
|
+
# 1. It first checks if the key exists in Redis. This is crucial because:
|
167
|
+
# - It provides a definitive answer about the object's existence.
|
168
|
+
# - It prevents ambiguity that could arise from `hgetall` returning an
|
169
|
+
# empty hash for non-existent keys, which could lead to the creation
|
170
|
+
# of "empty" objects.
|
171
|
+
#
|
172
|
+
# 2. If the key exists, it retrieves the object's data and instantiates
|
173
|
+
# it.
|
174
|
+
#
|
175
|
+
# This approach ensures that we only attempt to instantiate objects that
|
176
|
+
# actually exist in Redis, improving reliability and simplifying
|
177
|
+
# debugging.
|
178
|
+
#
|
179
|
+
# @example
|
180
|
+
# User.from_key("user:123") # Returns a User instance if it exists,
|
181
|
+
# nil otherwise
|
182
|
+
#
|
148
183
|
def from_key(objkey)
|
149
184
|
raise ArgumentError, 'Empty key' if objkey.to_s.empty?
|
150
185
|
|
@@ -153,27 +188,46 @@ module Familia
|
|
153
188
|
does_exist = redis.exists(objkey).positive?
|
154
189
|
|
155
190
|
Familia.ld "[.from_key] #{self} from key #{objkey} (exists: #{does_exist})"
|
156
|
-
Familia.trace :
|
191
|
+
Familia.trace :FROM_KEY, redis, objkey, caller if Familia.debug?
|
157
192
|
|
158
|
-
# This is reason for calling exists first. We want to definitively
|
159
|
-
# ambiguity know if the object exists in Redis. If it
|
160
|
-
# it does, we proceed to load the object.
|
161
|
-
#
|
162
|
-
# debug.
|
193
|
+
# This is the reason for calling exists first. We want to definitively
|
194
|
+
# and without any ambiguity know if the object exists in Redis. If it
|
195
|
+
# doesn't, we return nil. If it does, we proceed to load the object.
|
196
|
+
# Otherwise, hgetall will return an empty hash, which will be passed to
|
197
|
+
# the constructor, which will then be annoying to debug.
|
163
198
|
return unless does_exist
|
164
199
|
|
165
200
|
obj = redis.hgetall(objkey) # horreum objects are persisted as redis hashes
|
166
|
-
Familia.trace :
|
201
|
+
Familia.trace :FROM_KEY2, redis, "#{objkey}: #{obj.inspect}", caller if Familia.debug?
|
167
202
|
|
168
203
|
new(**obj)
|
169
204
|
end
|
170
205
|
|
206
|
+
# Retrieves and instantiates an object from Redis using its identifier.
|
207
|
+
#
|
208
|
+
# @param identifier [String, Integer] The unique identifier for the
|
209
|
+
# object.
|
210
|
+
# @param suffix [Symbol] The suffix to use in the Redis key (default:
|
211
|
+
# :object).
|
212
|
+
# @return [Object, nil] An instance of the class if found, nil otherwise.
|
213
|
+
#
|
214
|
+
# This method constructs the full Redis key using the provided identifier
|
215
|
+
# and suffix, then delegates to `from_key` for the actual retrieval and
|
216
|
+
# instantiation.
|
217
|
+
#
|
218
|
+
# It's a higher-level method that abstracts away the key construction,
|
219
|
+
# making it easier to retrieve objects when you only have their
|
220
|
+
# identifier.
|
221
|
+
#
|
222
|
+
# @example
|
223
|
+
# User.from_redis(123) # Equivalent to User.from_key("user:123:object")
|
224
|
+
#
|
171
225
|
def from_redis(identifier, suffix = :object)
|
172
226
|
return nil if identifier.to_s.empty?
|
173
227
|
|
174
228
|
objkey = rediskey(identifier, suffix)
|
175
229
|
Familia.ld "[.from_redis] #{self} from key #{objkey})"
|
176
|
-
Familia.trace :
|
230
|
+
Familia.trace :FROM_REDIS, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
|
177
231
|
from_key objkey
|
178
232
|
end
|
179
233
|
|
@@ -183,10 +237,7 @@ module Familia
|
|
183
237
|
objkey = rediskey identifier, suffix
|
184
238
|
|
185
239
|
ret = redis.exists objkey
|
186
|
-
if Familia.debug?
|
187
|
-
Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}",
|
188
|
-
caller
|
189
|
-
end
|
240
|
+
Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}", caller if Familia.debug?
|
190
241
|
ret.positive?
|
191
242
|
end
|
192
243
|
|
@@ -236,5 +287,6 @@ module Familia
|
|
236
287
|
@load_method || :from_json # Familia.load_method
|
237
288
|
end
|
238
289
|
end
|
290
|
+
# End of ClassMethods module
|
239
291
|
end
|
240
292
|
end
|
@@ -36,13 +36,60 @@ module Familia
|
|
36
36
|
redis.hdel rediskey, field
|
37
37
|
end
|
38
38
|
|
39
|
-
def redistype
|
39
|
+
def redistype
|
40
40
|
redis.type rediskey(suffix)
|
41
41
|
end
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
redis.
|
43
|
+
# Parity with RedisType#rename
|
44
|
+
def rename(newkey)
|
45
|
+
redis.rename rediskey, newkey
|
46
|
+
end
|
47
|
+
|
48
|
+
# For parity with RedisType#hgetall
|
49
|
+
def hgetall
|
50
|
+
Familia.trace :HGETALL, redis, redisuri, caller(1..1) if Familia.debug?
|
51
|
+
redis.hgetall rediskey(suffix)
|
52
|
+
end
|
53
|
+
alias all hgetall
|
54
|
+
|
55
|
+
def hget(field)
|
56
|
+
redis.hget rediskey(suffix), field
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return The number of fields that were added to the hash. If the
|
60
|
+
# field already exists, this will return 0.
|
61
|
+
def hset(field, value)
|
62
|
+
Familia.trace :HSET, redis, redisuri, caller(1..1) if Familia.debug?
|
63
|
+
redis.hset rediskey, field, value
|
64
|
+
end
|
65
|
+
|
66
|
+
def hmset
|
67
|
+
redis.hmset rediskey(suffix), self.to_h
|
68
|
+
end
|
69
|
+
|
70
|
+
def hkeys
|
71
|
+
Familia.trace :HKEYS, redis, 'redisuri', caller(1..1) if Familia.debug?
|
72
|
+
redis.hkeys rediskey(suffix)
|
73
|
+
end
|
74
|
+
|
75
|
+
def hvals
|
76
|
+
redis.hvals rediskey(suffix)
|
77
|
+
end
|
78
|
+
|
79
|
+
def hincrby(field, increment)
|
80
|
+
redis.hincrby rediskey(suffix), field, increment
|
81
|
+
end
|
82
|
+
|
83
|
+
def hincrbyfloat(field, increment)
|
84
|
+
redis.hincrbyfloat rediskey(suffix), field, increment
|
85
|
+
end
|
86
|
+
|
87
|
+
def hlen
|
88
|
+
redis.hlen rediskey(suffix)
|
89
|
+
end
|
90
|
+
|
91
|
+
def hstrlen(field)
|
92
|
+
redis.hstrlen rediskey(suffix), field
|
46
93
|
end
|
47
94
|
|
48
95
|
def delete!
|
@@ -10,6 +10,12 @@ module Familia
|
|
10
10
|
|
11
11
|
# Methods that call load and dump (InstanceMethods)
|
12
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.
|
13
19
|
module Serialization
|
14
20
|
#include Familia::RedisType::Serialization
|
15
21
|
|
@@ -38,8 +44,9 @@ module Familia
|
|
38
44
|
Familia.trace :SAVE, redis, redisuri, caller(1..1) if Familia.debug?
|
39
45
|
|
40
46
|
# Update timestamp fields
|
47
|
+
self.key ||= self.identifier
|
41
48
|
self.updated = Familia.now.to_i
|
42
|
-
self.created
|
49
|
+
self.created ||= Familia.now.to_i
|
43
50
|
|
44
51
|
# Thr return value of commit_fields is an array of strings: ["OK"].
|
45
52
|
ret = commit_fields # e.g. ["OK"]
|
@@ -64,12 +71,39 @@ module Familia
|
|
64
71
|
delete!
|
65
72
|
end
|
66
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
|
+
|
67
101
|
def to_h
|
68
102
|
# Use self.class.fields to efficiently generate a hash
|
69
103
|
# of all the fields for this object
|
70
104
|
self.class.fields.inject({}) do |hsh, field|
|
71
105
|
val = send(field)
|
72
|
-
prepared = val
|
106
|
+
prepared = to_redis(val)
|
73
107
|
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared.class}"
|
74
108
|
hsh[field] = prepared
|
75
109
|
hsh
|
@@ -79,13 +113,14 @@ module Familia
|
|
79
113
|
def to_a
|
80
114
|
self.class.fields.map do |field|
|
81
115
|
val = send(field)
|
82
|
-
|
83
|
-
|
116
|
+
prepared = to_redis(val)
|
117
|
+
Familia.ld " [to_a] field: #{field} val: #{val.class} prepared: #{prepared.class}"
|
118
|
+
prepared
|
84
119
|
end
|
85
120
|
end
|
86
121
|
|
87
|
-
# The to_redis method in Familia::Redistype and Familia::Horreum serve
|
88
|
-
# but have some key differences in their implementation:
|
122
|
+
# The to_redis method in Familia::Redistype and Familia::Horreum serve
|
123
|
+
# similar purposes but have some key differences in their implementation:
|
89
124
|
#
|
90
125
|
# Similarities:
|
91
126
|
# - Both methods aim to serialize various data types for Redis storage
|
@@ -97,16 +132,19 @@ module Familia
|
|
97
132
|
# - Familia::Horreum had more explicit type checking and conversion
|
98
133
|
# - Familia::Redistype includes more extensive debug tracing
|
99
134
|
#
|
100
|
-
# The centralized Familia.distinguisher method accommodates both approaches
|
101
|
-
#
|
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
|
102
139
|
# 2. Providing a 'strict_values' option for flexible type handling
|
103
140
|
# 3. Supporting custom serialization through a dump_method
|
104
141
|
# 4. Including debug tracing similar to Familia::Redistype
|
105
142
|
#
|
106
|
-
# By using Familia.distinguisher, we achieve more consistent behavior
|
107
|
-
# different parts of the library while maintaining the flexibility
|
108
|
-
# various data types and custom serialization needs. This
|
109
|
-
# also makes it easier to extend or modify serialization
|
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.
|
110
148
|
#
|
111
149
|
def to_redis(val)
|
112
150
|
prepared = Familia.distinguisher(val, false)
|
@@ -115,7 +153,10 @@ module Familia
|
|
115
153
|
prepared = val.send(dump_method)
|
116
154
|
end
|
117
155
|
|
118
|
-
|
156
|
+
if prepared.nil?
|
157
|
+
Familia.ld "[#{self.class}#to_redis] nil returned for #{self.class}##{name}"
|
158
|
+
end
|
159
|
+
|
119
160
|
prepared
|
120
161
|
end
|
121
162
|
|
@@ -127,6 +168,7 @@ module Familia
|
|
127
168
|
expire ttl.to_i
|
128
169
|
end
|
129
170
|
end
|
171
|
+
# End of Serialization module
|
130
172
|
|
131
173
|
include Serialization # these become Horreum instance methods
|
132
174
|
end
|
@@ -134,21 +176,18 @@ end
|
|
134
176
|
|
135
177
|
__END__
|
136
178
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
## NOTE: caching here like this only works if hsh has all keys
|
152
|
-
#self.cache.replace hsh
|
153
|
-
ret
|
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"
|
154
193
|
end
|
@@ -27,9 +27,10 @@ module Familia
|
|
27
27
|
# Whether this is a Horreum or RedisType object, the value is taken
|
28
28
|
# from the `identifier` method).
|
29
29
|
#
|
30
|
-
def rediskey(suffix =
|
30
|
+
def rediskey(suffix = nil, ignored = nil)
|
31
31
|
Familia.ld "[#rediskey] #{identifier} for #{self.class}"
|
32
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
|
33
34
|
self.class.rediskey identifier, suffix
|
34
35
|
end
|
35
36
|
|
data/lib/familia/horreum.rb
CHANGED
@@ -94,6 +94,15 @@ module Familia
|
|
94
94
|
# end
|
95
95
|
end
|
96
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
|
+
|
97
106
|
# Implementing classes can define an init method to do any
|
98
107
|
# additional initialization. Notice that this is called
|
99
108
|
# after the fields are set.
|
@@ -145,23 +154,64 @@ module Familia
|
|
145
154
|
end
|
146
155
|
end
|
147
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
|
148
164
|
def initialize_with_positional_args(*args)
|
149
|
-
|
150
|
-
|
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
|
151
171
|
end
|
152
172
|
end
|
153
173
|
private :initialize_with_positional_args
|
154
174
|
|
155
|
-
|
156
|
-
|
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|
|
157
186
|
# Redis will give us field names as strings back, but internally
|
158
|
-
# we use symbols. So we
|
159
|
-
value =
|
160
|
-
|
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
|
161
193
|
end
|
162
194
|
end
|
163
195
|
private :initialize_with_keyword_args
|
164
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
|
+
|
165
215
|
# Determines the unique identifier for the instance
|
166
216
|
# This method is used to generate Redis keys for the object
|
167
217
|
def identifier
|
@@ -183,7 +233,7 @@ module Familia
|
|
183
233
|
end
|
184
234
|
|
185
235
|
# If the unique_id is nil, raise an error
|
186
|
-
raise Problem, "Identifier is nil for #{self}" if unique_id.nil?
|
236
|
+
raise Problem, "Identifier is nil for #{self.class}" if unique_id.nil?
|
187
237
|
raise Problem, 'Identifier is empty' if unique_id.empty?
|
188
238
|
|
189
239
|
unique_id
|
data/lib/familia/logging.rb
CHANGED
@@ -3,23 +3,6 @@
|
|
3
3
|
require 'pathname'
|
4
4
|
require 'logger'
|
5
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
6
|
module Familia
|
24
7
|
@logger = Logger.new($stdout)
|
25
8
|
@logger.progname = name
|
@@ -38,7 +21,7 @@ module Familia
|
|
38
21
|
# variable `severity_letter` is arbitrary and could be anything.
|
39
22
|
severity_letter = Thread.current[:severity_letter] || severity_letter
|
40
23
|
|
41
|
-
"#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}: #{msg}
|
24
|
+
"#{severity_letter}, #{utc_datetime} #{pid} #{thread_id}: #{msg} [#{relative_path}:#{line}]\n"
|
42
25
|
end
|
43
26
|
|
44
27
|
# The Logging module provides a set of methods and constants for logging messages
|
@@ -120,7 +103,7 @@ module Familia
|
|
120
103
|
attr_reader :logger
|
121
104
|
|
122
105
|
# Gives our logger the ability to use our trace method.
|
123
|
-
|
106
|
+
using LoggerTraceRefinement if LoggerTraceRefinement::ENABLED
|
124
107
|
|
125
108
|
def info(*msg)
|
126
109
|
@logger.info(*msg)
|
@@ -156,14 +139,14 @@ module Familia
|
|
156
139
|
# @return [nil]
|
157
140
|
#
|
158
141
|
def trace(label, redis_instance, ident, context = nil)
|
159
|
-
return unless
|
142
|
+
return unless LoggerTraceRefinement::ENABLED
|
160
143
|
instance_id = redis_instance&.id
|
161
144
|
codeline = if context
|
162
145
|
context = [context].flatten
|
163
146
|
context.reject! { |line| line =~ %r{lib/familia} }
|
164
147
|
context.first
|
165
148
|
end
|
166
|
-
@logger.
|
149
|
+
@logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
|
167
150
|
end
|
168
151
|
|
169
152
|
end
|
data/lib/familia/redistype.rb
CHANGED
@@ -61,10 +61,10 @@ module Familia
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
attr_reader :
|
64
|
+
attr_reader :keystring, :parent, :opts
|
65
65
|
attr_writer :dump_method, :load_method
|
66
66
|
|
67
|
-
# +
|
67
|
+
# +keystring+: If parent is set, this will be used as the suffix
|
68
68
|
# for rediskey. Otherwise this becomes the value of the key.
|
69
69
|
# If this is an Array, the elements will be joined.
|
70
70
|
#
|
@@ -92,10 +92,10 @@ module Familia
|
|
92
92
|
#
|
93
93
|
# Uses the redis connection of the parent or the value of
|
94
94
|
# opts[:redis] or Familia.redis (in that order).
|
95
|
-
def initialize(
|
95
|
+
def initialize(keystring, opts = {})
|
96
96
|
#Familia.ld " [initializing] #{self.class} #{opts}"
|
97
|
-
@
|
98
|
-
@
|
97
|
+
@keystring = keystring
|
98
|
+
@keystring = @keystring.join(Familia.delim) if @keystring.is_a?(Array)
|
99
99
|
|
100
100
|
# Remove all keys from the opts that are not in the allowed list
|
101
101
|
@opts = opts || {}
|
@@ -112,7 +112,7 @@ module Familia
|
|
112
112
|
|
113
113
|
# Produces the full redis key for this object.
|
114
114
|
def rediskey
|
115
|
-
Familia.ld "[rediskey] #{
|
115
|
+
Familia.ld "[rediskey] #{keystring} for #{self.class} (#{opts})"
|
116
116
|
|
117
117
|
# Return the hardcoded key if it's set. This is useful for
|
118
118
|
# support legacy keys that aren't derived in the same way.
|
@@ -121,15 +121,15 @@ module Familia
|
|
121
121
|
if parent_instance?
|
122
122
|
# This is an instance-level redistype object so the parent instance's
|
123
123
|
# rediskey method is defined in Familia::Horreum::InstanceMethods.
|
124
|
-
parent.rediskey(
|
124
|
+
parent.rediskey(keystring)
|
125
125
|
elsif parent_class?
|
126
126
|
# This is a class-level redistype object so the parent class' rediskey
|
127
127
|
# method is defined in Familia::Horreum::ClassMethods.
|
128
|
-
parent.rediskey(
|
128
|
+
parent.rediskey(keystring, nil)
|
129
129
|
else
|
130
|
-
# This is a standalone RedisType object where it's
|
130
|
+
# This is a standalone RedisType object where it's keystring
|
131
131
|
# is the full key.
|
132
|
-
|
132
|
+
keystring
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
@@ -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
|
@@ -53,13 +53,12 @@ module Familia
|
|
53
53
|
multi_from_redis(*el)
|
54
54
|
end
|
55
55
|
|
56
|
-
def
|
56
|
+
def hgetall
|
57
57
|
# TODO: Use from_redis. Also name `all` is confusing with
|
58
58
|
# Onetime::Customer.all which returns all customers.
|
59
59
|
redis.hgetall rediskey
|
60
60
|
end
|
61
|
-
alias
|
62
|
-
alias clone all
|
61
|
+
alias all hgetall
|
63
62
|
|
64
63
|
def has_key?(field)
|
65
64
|
redis.hexists rediskey, field
|
data/lib/familia/utils.rb
CHANGED
@@ -64,8 +64,8 @@ module Familia
|
|
64
64
|
Time.at(rounded).utc.strftime(pattern)
|
65
65
|
end
|
66
66
|
|
67
|
-
def generate_sha_hash(elements)
|
68
|
-
concatenated_string = Familia.join(elements)
|
67
|
+
def generate_sha_hash(*elements)
|
68
|
+
concatenated_string = Familia.join(*elements)
|
69
69
|
DIGEST_CLASS.hexdigest(concatenated_string)
|
70
70
|
end
|
71
71
|
|
@@ -113,10 +113,6 @@ module Familia
|
|
113
113
|
Familia.trace :TOREDIS_DISTINGUISHER, redis, "isabase", caller(1..1) if Familia.debug?
|
114
114
|
value_to_distinguish.identifier
|
115
115
|
|
116
|
-
elsif dump_method && value_to_distinguish.respond_to?(dump_method)
|
117
|
-
Familia.trace :TOREDIS_DISTINGUISHER, redis, "#{value_to_distinguish.class}##{dump_method}", caller(1..1) if Familia.debug?
|
118
|
-
value_to_distinguish.send(dump_method)
|
119
|
-
|
120
116
|
else
|
121
117
|
Familia.trace :TOREDIS_DISTINGUISHER, redis, "else2 #{strict_values}", caller(1..1) if Familia.debug?
|
122
118
|
raise Familia::HighRiskFactor, value_to_distinguish if strict_values
|
data/lib/familia.rb
CHANGED
data/try/27_redis_horreum_try.rb
CHANGED
@@ -38,3 +38,56 @@ Familia.debug = true
|
|
38
38
|
## Remove the key
|
39
39
|
@hashkey.clear
|
40
40
|
#=> 1
|
41
|
+
|
42
|
+
## Horreum objects can update and save their fields (1 of 2)
|
43
|
+
@customer.name = 'John Doe'
|
44
|
+
#=> "John Doe"
|
45
|
+
|
46
|
+
## Horreum objects can update and save their fields (2 of 2)
|
47
|
+
@customer.save
|
48
|
+
#=> true
|
49
|
+
|
50
|
+
## Horreum object fields have a fast writer method (1 of 2)
|
51
|
+
Familia.trace :LOAD, @customer.redis, @customer.redisuri, caller if Familia.debug?
|
52
|
+
@customer.name! 'Jane Doe'
|
53
|
+
#=> 0
|
54
|
+
|
55
|
+
## Horreum object fields have a fast writer method (2 of 2)
|
56
|
+
@customer.refresh!
|
57
|
+
@customer.name
|
58
|
+
#=> "Jane Doe"
|
59
|
+
|
60
|
+
## Unsaved changes are lost when an object reloads
|
61
|
+
@customer.name = 'John Doe'
|
62
|
+
@customer.refresh!
|
63
|
+
@customer.name
|
64
|
+
#=> "Jane Doe"
|
65
|
+
|
66
|
+
## Horreum objects can be destroyed
|
67
|
+
@customer.destroy!
|
68
|
+
#=> true
|
69
|
+
|
70
|
+
## All horrerum objects have a key field
|
71
|
+
@customer.key
|
72
|
+
#=> @identifier
|
73
|
+
|
74
|
+
## Even ones that didn't define it
|
75
|
+
@cd = CustomDomain.new "www.example.com", "@identifier"
|
76
|
+
@cd.key
|
77
|
+
#=> nil
|
78
|
+
|
79
|
+
## We can call #identifier directly if we want to "lasy load" the unique identifier
|
80
|
+
@cd.identifier
|
81
|
+
#=> "7565befd"
|
82
|
+
|
83
|
+
## The #key field will still be nil
|
84
|
+
@cd.key
|
85
|
+
#=> nil
|
86
|
+
|
87
|
+
## But once we save
|
88
|
+
@cd.save
|
89
|
+
#=> true
|
90
|
+
|
91
|
+
## The key will be set
|
92
|
+
@cd.key
|
93
|
+
#=> "7565befd"
|
data/try/test_helpers.rb
CHANGED
@@ -70,6 +70,7 @@ class Customer < Familia::Horreum
|
|
70
70
|
field :email
|
71
71
|
field :role
|
72
72
|
field :key
|
73
|
+
field :name
|
73
74
|
field :passphrase_encryption
|
74
75
|
field :passphrase
|
75
76
|
field :verified
|
@@ -154,6 +155,7 @@ class CustomDomain < Familia::Horreum
|
|
154
155
|
field :trd
|
155
156
|
field :tld
|
156
157
|
field :sld
|
158
|
+
# No :key field (so we can test hte behaviour in Horreum#initialize)
|
157
159
|
field :txt_validation_host
|
158
160
|
field :txt_validation_value
|
159
161
|
field :status
|
@@ -167,7 +169,11 @@ class CustomDomain < Familia::Horreum
|
|
167
169
|
# the customer ID. This is used to ensure that the same domain can't be
|
168
170
|
# added twice by the same customer while avoiding collisions between customers.
|
169
171
|
def derive_id
|
170
|
-
|
172
|
+
elements = [
|
173
|
+
display_domain,
|
174
|
+
custid
|
175
|
+
]
|
176
|
+
Familia.generate_sha_hash(*elements).slice(0, 8)
|
171
177
|
end
|
172
178
|
end
|
173
179
|
@d = CustomDomain.new
|
@@ -178,6 +184,8 @@ class Limiter < Familia::Horreum
|
|
178
184
|
|
179
185
|
identifier :name
|
180
186
|
field :name
|
187
|
+
# No :key field (so we can test hte behaviour in Horreum#initialize)
|
188
|
+
|
181
189
|
string :counter, :ttl => 1.hour, :quantize => [10.minutes, '%H:%M', 1302468980]
|
182
190
|
|
183
191
|
def identifier
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: familia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.pre.
|
4
|
+
version: 1.0.0.pre.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-08-
|
11
|
+
date: 2024-08-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -97,6 +97,7 @@ files:
|
|
97
97
|
- lib/familia/redistype.rb
|
98
98
|
- lib/familia/redistype/commands.rb
|
99
99
|
- lib/familia/redistype/serialization.rb
|
100
|
+
- lib/familia/refinements.rb
|
100
101
|
- lib/familia/settings.rb
|
101
102
|
- lib/familia/types/hashkey.rb
|
102
103
|
- lib/familia/types/list.rb
|
@@ -141,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
142
|
- !ruby/object:Gem::Version
|
142
143
|
version: '0'
|
143
144
|
requirements: []
|
144
|
-
rubygems_version: 3.5.
|
145
|
+
rubygems_version: 3.5.15
|
145
146
|
signing_key:
|
146
147
|
specification_version: 4
|
147
148
|
summary: An ORM for Redis in Ruby.
|