familia 1.0.0.pre.rc1 → 1.0.0.pre.rc3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fcefc7fe4a1a64cc584d629612f752ecb1eca753ab39245217f017a03b71075e
4
- data.tar.gz: 8a2d0313578fe9acf743537efaee95e8167eabab98474a2a821222696641b567
3
+ metadata.gz: c5cdd5bc9ccadb7e69c324e7a2716b378fa5e5fe9938c4ed44a61b23eef99c6a
4
+ data.tar.gz: de5bb8d9e3b6b09906e777f9a5586eff32a5b1d3ec7c5e51a3a26dac2bd85415
5
5
  SHA512:
6
- metadata.gz: 3f5e8755442f3766515caf445e78888fa26a1b66910bd04228d2fee24988ffa1b380ea5aeedde02a5c545e74d58fee5e273801e7c3f5cc18e873093f383da42a
7
- data.tar.gz: 39d8c525f39c123982a150dc62e1cfd302b602086068ea7e694061bc889ab040e1765af5177e23fb9db5b408e2f4ec6f0b5d4ddf165d6ead6dce929a927a61d1
6
+ metadata.gz: 76ffa691585dde5c45aaa17e1d02171bacb3fad0267f638b02d7c2cf0a65eaed4d0062656be2496c27ff9bd9788839a71d8475f3e14577ef607684cd179209c1
7
+ data.tar.gz: 96d52e17f1c3f3092d4ec39ad0fbd1455ef54a902a2f2aaa65c5531d11cdd8b53ee50ef1fa0bd967bd4fa49fd4b0693d89031bcb6decb6cc2def5cb4e1d85c9a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- familia (1.0.0.pre.rc1)
4
+ familia (1.0.0.pre.rc2)
5
5
  redis (>= 4.8.1, < 6.0)
6
6
  uri-redis (~> 1.3)
7
7
 
data/README.md CHANGED
@@ -1,31 +1,84 @@
1
- # Familia - 1.0.0-rc1
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-rc1'`
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
- class Flower < Familia::Horreum
17
- identifier :generate_id
18
- field :token
19
- field :name
20
- list :owners
21
- set :tags
22
- zset :metrics
23
- hashkey :props
24
- string :counter
25
- end
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
@@ -2,4 +2,4 @@
2
2
  :MAJOR: 1
3
3
  :MINOR: 0
4
4
  :PATCH: 0
5
- :PRE: rc1
5
+ :PRE: rc3
@@ -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
- # the hash syntax allows you to:
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 store in
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
 
@@ -37,23 +36,89 @@ module Familia
37
36
  attr_accessor :parent
38
37
  attr_writer :redis, :dump_method, :load_method
39
38
 
39
+ # Returns the Redis connection for the class.
40
+ #
41
+ # This method retrieves the Redis connection instance for the class. If no
42
+ # connection is set, it initializes a new connection using the provided URI
43
+ # or database configuration.
44
+ #
45
+ # @return [Redis] the Redis connection instance.
46
+ #
40
47
  def redis
41
48
  @redis || Familia.redis(uri || db)
42
49
  end
43
50
 
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.
51
+ # Sets or retrieves the unique identifier for the class.
52
+ #
53
+ # This method defines or returns the unique identifier used to generate the
54
+ # Redis key for the object. If a value is provided, it sets the identifier;
55
+ # otherwise, it returns the current identifier.
56
+ #
57
+ # @param [Object] val the value to set as the identifier (optional).
58
+ # @return [Object] the current identifier.
59
+ #
47
60
  def identifier(val = nil)
48
61
  @identifier = val if val
49
62
  @identifier
50
63
  end
51
64
 
52
- # Define a field for the class. This will create getter and setter
53
- # instance methods just like any "attr_accessor" methods.
65
+ # Defines a field for the class and creates accessor methods.
66
+ #
67
+ # This method defines a new field for the class, creating getter and setter
68
+ # instance methods similar to `attr_accessor`. It also generates a fast
69
+ # writer method for immediate persistence to Redis.
70
+ #
71
+ # @param [Symbol, String] name the name of the field to define.
72
+ #
54
73
  def field(name)
55
74
  fields << name
56
75
  attr_accessor name
76
+
77
+ # Every field gets a fast writer method for immediately persisting
78
+ fast_writer! name
79
+ end
80
+
81
+ # Defines a writer method with a bang (!) suffix for a given attribute name.
82
+ #
83
+ # The dynamically defined method performs the following:
84
+ # - Checks if the correct number of arguments is provided (exactly one).
85
+ # - Converts the provided value to a format suitable for Redis storage.
86
+ # - Uses the existing accessor method to set the attribute value.
87
+ # - Persists the value to Redis immediately using the hset command.
88
+ # - Includes custom error handling to raise an ArgumentError if the wrong number of arguments is given.
89
+ # - Raises a custom error message if an exception occurs during the execution of the method.
90
+ #
91
+ # @param [Symbol, String] name the name of the attribute for which the writer method is defined.
92
+ # @raise [ArgumentError] if the wrong number of arguments is provided.
93
+ # @raise [RuntimeError] if an exception occurs during the execution of the method.
94
+ #
95
+ def fast_writer!(name)
96
+ define_method :"#{name}!" do |*args|
97
+ # Check if the correct number of arguments is provided (exactly one).
98
+ if args.size != 1
99
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
100
+ end
101
+
102
+ value = args.first
103
+
104
+ begin
105
+ # Trace the operation if debugging is enabled.
106
+ Familia.trace :FAST_WRITER, redis, "#{name}: #{value.inspect}", caller if Familia.debug?
107
+
108
+ # Convert the provided value to a format suitable for Redis storage.
109
+ prepared = to_redis(value)
110
+ Familia.ld "[.fast_writer!] #{name} val: #{value.class} prepared: #{prepared.class}"
111
+
112
+ # Use the existing accessor method to set the attribute value.
113
+ send :"#{name}=", value
114
+
115
+ # Persist the value to Redis immediately using the hset command.
116
+ hset name, prepared
117
+ rescue Familia::Problem => e
118
+ # Raise a custom error message if an exception occurs during the execution of the method.
119
+ raise "#{name}! method failed: #{e.message}", e.backtrace
120
+ end
121
+ end
57
122
  end
58
123
 
59
124
  # Returns the list of field names defined for the class in the order
@@ -81,11 +146,6 @@ module Familia
81
146
  @redis_types
82
147
  end
83
148
 
84
- def defined_fields
85
- @defined_fields ||= {}
86
- @defined_fields
87
- end
88
-
89
149
  def ttl(v = nil)
90
150
  @ttl = v unless v.nil?
91
151
  @ttl || parent&.ttl
@@ -145,6 +205,34 @@ module Familia
145
205
  redis.mget(*ids)
146
206
  end
147
207
 
208
+ # Retrieves and instantiates an object from Redis using the full object
209
+ # key.
210
+ #
211
+ # @param objkey [String] The full Redis key for the object.
212
+ # @return [Object, nil] An instance of the class if the key exists, nil
213
+ # otherwise.
214
+ # @raise [ArgumentError] If the provided key is empty.
215
+ #
216
+ # This method performs a two-step process to safely retrieve and
217
+ # instantiate objects:
218
+ #
219
+ # 1. It first checks if the key exists in Redis. This is crucial because:
220
+ # - It provides a definitive answer about the object's existence.
221
+ # - It prevents ambiguity that could arise from `hgetall` returning an
222
+ # empty hash for non-existent keys, which could lead to the creation
223
+ # of "empty" objects.
224
+ #
225
+ # 2. If the key exists, it retrieves the object's data and instantiates
226
+ # it.
227
+ #
228
+ # This approach ensures that we only attempt to instantiate objects that
229
+ # actually exist in Redis, improving reliability and simplifying
230
+ # debugging.
231
+ #
232
+ # @example
233
+ # User.from_key("user:123") # Returns a User instance if it exists,
234
+ # nil otherwise
235
+ #
148
236
  def from_key(objkey)
149
237
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
150
238
 
@@ -153,27 +241,46 @@ module Familia
153
241
  does_exist = redis.exists(objkey).positive?
154
242
 
155
243
  Familia.ld "[.from_key] #{self} from key #{objkey} (exists: #{does_exist})"
156
- Familia.trace :LOAD, redis, objkey, caller if Familia.debug?
244
+ Familia.trace :FROM_KEY, redis, objkey, caller if Familia.debug?
157
245
 
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.
246
+ # This is the reason for calling exists first. We want to definitively
247
+ # and without any ambiguity know if the object exists in Redis. If it
248
+ # doesn't, we return nil. If it does, we proceed to load the object.
249
+ # Otherwise, hgetall will return an empty hash, which will be passed to
250
+ # the constructor, which will then be annoying to debug.
163
251
  return unless does_exist
164
252
 
165
253
  obj = redis.hgetall(objkey) # horreum objects are persisted as redis hashes
166
- Familia.trace :HGETALL, redis, "#{objkey}: #{obj.inspect}", caller if Familia.debug?
254
+ Familia.trace :FROM_KEY2, redis, "#{objkey}: #{obj.inspect}", caller if Familia.debug?
167
255
 
168
256
  new(**obj)
169
257
  end
170
258
 
259
+ # Retrieves and instantiates an object from Redis using its identifier.
260
+ #
261
+ # @param identifier [String, Integer] The unique identifier for the
262
+ # object.
263
+ # @param suffix [Symbol] The suffix to use in the Redis key (default:
264
+ # :object).
265
+ # @return [Object, nil] An instance of the class if found, nil otherwise.
266
+ #
267
+ # This method constructs the full Redis key using the provided identifier
268
+ # and suffix, then delegates to `from_key` for the actual retrieval and
269
+ # instantiation.
270
+ #
271
+ # It's a higher-level method that abstracts away the key construction,
272
+ # making it easier to retrieve objects when you only have their
273
+ # identifier.
274
+ #
275
+ # @example
276
+ # User.from_redis(123) # Equivalent to User.from_key("user:123:object")
277
+ #
171
278
  def from_redis(identifier, suffix = :object)
172
279
  return nil if identifier.to_s.empty?
173
280
 
174
281
  objkey = rediskey(identifier, suffix)
175
282
  Familia.ld "[.from_redis] #{self} from key #{objkey})"
176
- Familia.trace :FROMREDIS, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
283
+ Familia.trace :FROM_REDIS, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
177
284
  from_key objkey
178
285
  end
179
286
 
@@ -183,10 +290,7 @@ module Familia
183
290
  objkey = rediskey identifier, suffix
184
291
 
185
292
  ret = redis.exists objkey
186
- if Familia.debug?
187
- Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}",
188
- caller
189
- end
293
+ Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}", caller if Familia.debug?
190
294
  ret.positive?
191
295
  end
192
296
 
@@ -236,5 +340,6 @@ module Familia
236
340
  @load_method || :from_json # Familia.load_method
237
341
  end
238
342
  end
343
+ # End of ClassMethods module
239
344
  end
240
345
  end
@@ -20,9 +20,12 @@ module Familia
20
20
 
21
21
  def exists?
22
22
  ret = redis.exists rediskey
23
- ret.positive?
23
+ ret.positive? # differs from redis API but I think it's okay bc `exists?` is a predicate method.
24
24
  end
25
25
 
26
+ # Sets a timeout on key. After the timeout has expired, the key will automatically be deleted.
27
+ # Returns 1 if the timeout was set, 0 if key does not exist or the timeout could not be set.
28
+ #
26
29
  def expire(ttl = nil)
27
30
  ttl ||= self.class.ttl
28
31
  redis.expire rediskey, ttl.to_i
@@ -32,18 +35,94 @@ module Familia
32
35
  redis.ttl rediskey
33
36
  end
34
37
 
38
+ # Deletes a field from the hash stored at the Redis key.
39
+ #
40
+ # @param field [String] The field to delete from the hash.
41
+ # @return [Integer] The number of fields that were removed from the hash (0 or 1).
42
+ # @note This method is destructive, as indicated by the bang (!).
35
43
  def hdel!(field)
36
44
  redis.hdel rediskey, field
37
45
  end
38
46
 
39
- def redistype(suffix = nil)
47
+ def redistype
40
48
  redis.type rediskey(suffix)
41
49
  end
42
50
 
43
- def hmset(suffix = nil)
44
- suffix ||= self.class.suffix
45
- redis.hmset rediskey(suffix), to_h
51
+ # Parity with RedisType#rename
52
+ def rename(newkey)
53
+ redis.rename rediskey, newkey
54
+ end
55
+
56
+ # For parity with RedisType#hgetall
57
+ def hgetall
58
+ Familia.trace :HGETALL, redis, redisuri, caller(1..1) if Familia.debug?
59
+ redis.hgetall rediskey(suffix)
60
+ end
61
+ alias all hgetall
62
+
63
+ def hget(field)
64
+ redis.hget rediskey(suffix), field
65
+ end
66
+
67
+ # @return The number of fields that were added to the hash. If the
68
+ # field already exists, this will return 0.
69
+ def hset(field, value)
70
+ Familia.trace :HSET, redis, redisuri, caller(1..1) if Familia.debug?
71
+ redis.hset rediskey, field, value
72
+ end
73
+
74
+ def hmset
75
+ redis.hmset rediskey(suffix), self.to_h
76
+ end
77
+
78
+ def hkeys
79
+ Familia.trace :HKEYS, redis, 'redisuri', caller(1..1) if Familia.debug?
80
+ redis.hkeys rediskey(suffix)
81
+ end
82
+
83
+ def hvals
84
+ redis.hvals rediskey(suffix)
85
+ end
86
+
87
+ def incr(field)
88
+ redis.hincrby rediskey(suffix), field, 1
89
+ end
90
+ alias increment incr
91
+
92
+ def incrby(field, increment)
93
+ redis.hincrby rediskey(suffix), field, increment
94
+ end
95
+ alias incrementby incrby
96
+
97
+ def incrbyfloat(field, increment)
98
+ redis.hincrbyfloat rediskey(suffix), field, increment
99
+ end
100
+ alias incrementbyfloat incrbyfloat
101
+
102
+ def decrby(field, decrement)
103
+ redis.decrby rediskey(suffix), field, decrement
104
+ end
105
+ alias decrementby decrby
106
+
107
+ def decr(field)
108
+ redis.hdecr field
109
+ end
110
+ alias decrement decr
111
+
112
+ def hlen
113
+ redis.hlen rediskey(suffix)
114
+ end
115
+ alias hlength hlen
116
+
117
+ def hstrlen(field)
118
+ redis.hstrlen rediskey(suffix), field
119
+ end
120
+ alias hstrlength hstrlen
121
+
122
+ def key?(field)
123
+ redis.hexists rediskey(suffix), field
46
124
  end
125
+ alias has_key? key?
47
126
 
48
127
  def delete!
49
128
  Familia.trace :DELETE!, redis, redisuri, caller(1..1) if Familia.debug?