familia 1.1.0.pre.rc1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.pre-commit-config.yaml +1 -1
- data/VERSION.yml +1 -2
- data/lib/familia/horreum/commands.rb +18 -14
- data/lib/familia/horreum/serialization.rb +99 -7
- data/lib/familia/horreum.rb +64 -18
- data/lib/familia/redistype/types/hashkey.rb +8 -7
- data/lib/familia/redistype/types/sorted_set.rb +0 -1
- data/lib/familia.rb +1 -0
- data/try/28_redis_horreum_serialization_try.rb +159 -0
- data/try/29_redis_horreum_initialization_try.rb +113 -0
- data/try/91_json_bug_try.rb +86 -0
- data/try/92_symbolize_try.rb +96 -0
- data/try/93_string_coercion_try.rb +154 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a664f87a493fffb0e362752c8f988c8de27d83008f3a796b66737e3d15e771d
|
4
|
+
data.tar.gz: cb22d85f774e40a7b656713760f2f1e31b154171620038c282ce2dbe5e88492b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3942bab094053731bd0d45c47a89c7bde62d26b99339cd2fad36e5481b28f91cd3eecb17c8ef6f77e3e486858151f6d5821e973f9fb7c068431abf11634806db
|
7
|
+
data.tar.gz: 32933d4adbb991b3cb790f4a47134f7e428fa617b3f33d3068d98a2e795d2ee58b2654ac22fb913abdc46dd0f3459bcdfd50d71273a137d2f337c4da521e475d
|
data/.gitignore
CHANGED
data/.pre-commit-config.yaml
CHANGED
data/VERSION.yml
CHANGED
@@ -22,22 +22,26 @@ module Familia
|
|
22
22
|
redis.move rediskey, db
|
23
23
|
end
|
24
24
|
|
25
|
-
# Checks if the calling object's key exists in Redis
|
25
|
+
# Checks if the calling object's key exists in Redis.
|
26
26
|
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
27
|
+
# @param check_size [Boolean] When true (default), also verifies the hash has a non-zero size.
|
28
|
+
# When false, only checks key existence regardless of content.
|
29
|
+
# @return [Boolean] Returns `true` if the key exists in Redis. When `check_size` is true,
|
30
|
+
# also requires the hash to have at least one field.
|
31
31
|
#
|
32
|
-
# @
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
32
|
+
# @example Check existence with size validation (default behavior)
|
33
|
+
# some_object.exists? # => false for empty hashes
|
34
|
+
# some_object.exists?(check_size: true) # => false for empty hashes
|
35
|
+
#
|
36
|
+
# @example Check existence only
|
37
|
+
# some_object.exists?(check_size: false) # => true for empty hashes
|
38
|
+
#
|
39
|
+
# @note The default behavior maintains backward compatibility by treating empty hashes
|
40
|
+
# as non-existent. Use `check_size: false` for pure key existence checking.
|
41
|
+
def exists?(check_size: true)
|
42
|
+
key_exists = self.class.redis.exists?(rediskey)
|
43
|
+
return key_exists unless check_size
|
44
|
+
key_exists && !size.zero?
|
41
45
|
end
|
42
46
|
|
43
47
|
# Returns the number of fields in the main object hash
|
@@ -121,8 +121,9 @@ module Familia
|
|
121
121
|
def save update_expiration: true
|
122
122
|
Familia.trace :SAVE, redis, redisuri, caller(1..1) if Familia.debug?
|
123
123
|
|
124
|
-
# Update our object's life story
|
125
|
-
|
124
|
+
# Update our object's life story, keeping the mandatory built-in
|
125
|
+
# key field in sync with the field that is the chosen identifier.
|
126
|
+
self.key = self.identifier
|
126
127
|
self.created ||= Familia.now.to_i if respond_to?(:created)
|
127
128
|
self.updated = Familia.now.to_i if respond_to?(:updated)
|
128
129
|
|
@@ -137,6 +138,41 @@ module Familia
|
|
137
138
|
ret.successful?
|
138
139
|
end
|
139
140
|
|
141
|
+
# Updates multiple fields atomically in a Redis transaction.
|
142
|
+
#
|
143
|
+
# @param fields [Hash] Field names and values to update. Special key :update_expiration
|
144
|
+
# controls whether to update key expiration (default: true)
|
145
|
+
# @return [MultiResult] Transaction result
|
146
|
+
#
|
147
|
+
# @example Update multiple fields without affecting expiration
|
148
|
+
# metadata.batch_update(viewed: 1, updated: Time.now.to_i, update_expiration: false)
|
149
|
+
#
|
150
|
+
# @example Update fields with expiration refresh
|
151
|
+
# user.batch_update(name: "John", email: "john@example.com")
|
152
|
+
#
|
153
|
+
def batch_update(**kwargs)
|
154
|
+
update_expiration = kwargs.delete(:update_expiration) { true }
|
155
|
+
fields = kwargs
|
156
|
+
|
157
|
+
Familia.trace :BATCH_UPDATE, redis, fields.keys, caller(1..1) if Familia.debug?
|
158
|
+
|
159
|
+
command_return_values = transaction do |conn|
|
160
|
+
fields.each do |field, value|
|
161
|
+
prepared_value = serialize_value(value)
|
162
|
+
conn.hset rediskey, field, prepared_value
|
163
|
+
# Update instance variable to keep object in sync
|
164
|
+
send("#{field}=", value) if respond_to?("#{field}=")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Update expiration if requested and supported
|
169
|
+
self.update_expiration(ttl: nil) if update_expiration && respond_to?(:update_expiration)
|
170
|
+
|
171
|
+
# Return same MultiResult format as other methods
|
172
|
+
summary_boolean = command_return_values.all? { |ret| %w[OK 0 1].include?(ret.to_s) }
|
173
|
+
MultiResult.new(summary_boolean, command_return_values)
|
174
|
+
end
|
175
|
+
|
140
176
|
# Apply a smattering of fields to this object like fairy dust.
|
141
177
|
#
|
142
178
|
# @param fields [Hash] A magical bag of named attributes to sprinkle onto
|
@@ -267,6 +303,26 @@ module Familia
|
|
267
303
|
delete!
|
268
304
|
end
|
269
305
|
|
306
|
+
# The Great Nilpocalypse: clear_fields!
|
307
|
+
#
|
308
|
+
# Imagine your object as a grand old mansion, every room stuffed with
|
309
|
+
# trinkets, secrets, and the odd rubber duck. This method? It flings open
|
310
|
+
# every window and lets a wild wind of nothingness sweep through, leaving
|
311
|
+
# each field as empty as a poet’s wallet.
|
312
|
+
#
|
313
|
+
# All your precious attributes—gone! Swept into the void! It’s a spring
|
314
|
+
# cleaning for the soul, a reset button for your existential dread.
|
315
|
+
#
|
316
|
+
# @return [void] Nothing left but echoes and nils.
|
317
|
+
#
|
318
|
+
# @example The Vanishing Act
|
319
|
+
# wizard.clear_fields!
|
320
|
+
# # => All fields are now nil, like a spell gone slightly too well.
|
321
|
+
#
|
322
|
+
def clear_fields!
|
323
|
+
self.class.fields.each { |field| send("#{field}=", nil) }
|
324
|
+
end
|
325
|
+
|
270
326
|
# The Great Redis Refresh-o-matic 3000
|
271
327
|
#
|
272
328
|
# Imagine your object as a forgetful time traveler. This method is like
|
@@ -335,8 +391,10 @@ module Familia
|
|
335
391
|
self.class.fields.inject({}) do |hsh, field|
|
336
392
|
val = send(field)
|
337
393
|
prepared = serialize_value(val)
|
338
|
-
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared
|
339
|
-
|
394
|
+
Familia.ld " [to_h] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
|
395
|
+
|
396
|
+
# Only include non-nil values in the hash for Redis
|
397
|
+
hsh[field] = prepared unless prepared.nil?
|
340
398
|
hsh
|
341
399
|
end
|
342
400
|
end
|
@@ -403,18 +461,47 @@ module Familia
|
|
403
461
|
def serialize_value(val)
|
404
462
|
prepared = Familia.distinguisher(val, strict_values: false)
|
405
463
|
|
406
|
-
|
407
|
-
|
464
|
+
# If the distinguisher returns nil, try using the dump_method but only
|
465
|
+
# use JSON serialization for complex types that need it.
|
466
|
+
if prepared.nil? && (val.is_a?(Hash) || val.is_a?(Array))
|
467
|
+
prepared = val.respond_to?(dump_method) ? val.send(dump_method) : JSON.dump(val)
|
408
468
|
end
|
409
469
|
|
470
|
+
# If both the distinguisher and dump_method return nil, log an error
|
410
471
|
if prepared.nil?
|
411
|
-
Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}
|
472
|
+
Familia.ld "[#{self.class}#serialize_value] nil returned for #{self.class}"
|
412
473
|
end
|
413
474
|
|
414
475
|
prepared
|
415
476
|
end
|
416
477
|
alias to_redis serialize_value
|
417
478
|
|
479
|
+
# Converts a Redis string value back to its original Ruby type
|
480
|
+
#
|
481
|
+
# This method attempts to deserialize JSON strings back to their original
|
482
|
+
# Hash or Array types. Simple string values are returned as-is.
|
483
|
+
#
|
484
|
+
# @param val [String] The string value from Redis to deserialize
|
485
|
+
# @param symbolize_keys [Boolean] Whether to symbolize hash keys (default: true for compatibility)
|
486
|
+
# @return [Object] The deserialized value (Hash, Array, or original string)
|
487
|
+
#
|
488
|
+
def deserialize_value(val, symbolize: true)
|
489
|
+
return val if val.nil? || val == ""
|
490
|
+
|
491
|
+
# Try to parse as JSON first for complex types
|
492
|
+
begin
|
493
|
+
parsed = JSON.parse(val, symbolize_names: symbolize)
|
494
|
+
# Only return parsed value if it's a complex type (Hash/Array)
|
495
|
+
# Simple values should remain as strings
|
496
|
+
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
497
|
+
rescue JSON::ParserError
|
498
|
+
# Not valid JSON, return as-is
|
499
|
+
end
|
500
|
+
|
501
|
+
val
|
502
|
+
end
|
503
|
+
alias from_redis deserialize_value
|
504
|
+
|
418
505
|
end
|
419
506
|
# End of Serialization module
|
420
507
|
|
@@ -474,6 +561,11 @@ module Familia
|
|
474
561
|
def tuple
|
475
562
|
[successful?, results]
|
476
563
|
end
|
564
|
+
alias to_a tuple
|
565
|
+
|
566
|
+
def to_h
|
567
|
+
{ success: successful?, results: results }
|
568
|
+
end
|
477
569
|
|
478
570
|
# Convenient method to check if the commit was successful.
|
479
571
|
#
|
data/lib/familia/horreum.rb
CHANGED
@@ -72,7 +72,14 @@ module Familia
|
|
72
72
|
end
|
73
73
|
|
74
74
|
# Instance initialization
|
75
|
-
# This method sets up the object's state, including Redis-related data
|
75
|
+
# This method sets up the object's state, including Redis-related data.
|
76
|
+
#
|
77
|
+
# Usage:
|
78
|
+
#
|
79
|
+
# Session.new("abc123", "user456") # positional (brittle)
|
80
|
+
# Session.new(sessid: "abc123", custid: "user456") # hash (robust)
|
81
|
+
# Session.new({sessid: "abc123", custid: "user456"}) # legacy hash (robust)
|
82
|
+
#
|
76
83
|
def initialize(*args, **kwargs)
|
77
84
|
Familia.ld "[Horreum] Initializing #{self.class}"
|
78
85
|
initialize_relatives
|
@@ -81,7 +88,7 @@ module Familia
|
|
81
88
|
# that every object horreum class has a unique identifier field. Ideally
|
82
89
|
# this logic would live somewhere else b/c we only need to call it once
|
83
90
|
# per class definition. Here it gets called every time an instance is
|
84
|
-
# instantiated
|
91
|
+
# instantiated.
|
85
92
|
unless self.class.fields.include?(:key)
|
86
93
|
# Define the 'key' field for this class
|
87
94
|
# This approach allows flexibility in how identifiers are generated
|
@@ -89,25 +96,45 @@ module Familia
|
|
89
96
|
self.class.field :key
|
90
97
|
end
|
91
98
|
|
92
|
-
#
|
93
|
-
|
99
|
+
# Detect if first argument is a hash (legacy support)
|
100
|
+
if args.size == 1 && args.first.is_a?(Hash) && kwargs.empty?
|
101
|
+
kwargs = args.first
|
102
|
+
args = []
|
103
|
+
end
|
104
|
+
|
105
|
+
# Initialize object with arguments using one of three strategies:
|
94
106
|
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
|
100
|
-
|
101
|
-
|
107
|
+
# 1. **Keyword Arguments** (Recommended): Order-independent field assignment
|
108
|
+
# Example: Customer.new(name: "John", email: "john@example.com")
|
109
|
+
# - Robust against field reordering
|
110
|
+
# - Self-documenting
|
111
|
+
# - Only sets provided fields
|
112
|
+
#
|
113
|
+
# 2. **Positional Arguments** (Legacy): Field assignment by definition order
|
114
|
+
# Example: Customer.new("john@example.com", "password123")
|
115
|
+
# - Brittle: breaks if field order changes
|
116
|
+
# - Compact syntax
|
117
|
+
# - Maps to fields in class definition order
|
118
|
+
#
|
119
|
+
# 3. **No Arguments**: Object created with all fields as nil
|
120
|
+
# - Minimal memory footprint in Redis
|
121
|
+
# - Fields set on-demand via accessors or save()
|
122
|
+
# - Avoids default value conflicts with nil-skipping serialization
|
123
|
+
#
|
124
|
+
# Note: We iterate over self.class.fields (not kwargs) to ensure only
|
125
|
+
# defined fields are set, preventing typos from creating undefined attributes.
|
126
|
+
#
|
127
|
+
if kwargs.any?
|
102
128
|
initialize_with_keyword_args(**kwargs)
|
129
|
+
elsif args.any?
|
130
|
+
initialize_with_positional_args(*args)
|
103
131
|
else
|
104
132
|
Familia.ld "[Horreum] #{self.class} initialized with no arguments"
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# end
|
133
|
+
# Default values are intentionally NOT set here to:
|
134
|
+
# - Maintain Redis memory efficiency (only store non-nil values)
|
135
|
+
# - Avoid conflicts with nil-skipping serialization logic
|
136
|
+
# - Preserve consistent exists? behavior (empty vs default-filled objects)
|
137
|
+
# - Keep initialization lightweight for unused fields
|
111
138
|
end
|
112
139
|
|
113
140
|
# Implementing classes can define an init method to do any
|
@@ -203,6 +230,12 @@ module Familia
|
|
203
230
|
end
|
204
231
|
private :initialize_with_keyword_args
|
205
232
|
|
233
|
+
def initialize_with_keyword_args_from_redis(**fields)
|
234
|
+
# Deserialize Redis string values back to their original types
|
235
|
+
deserialized_fields = fields.transform_values { |value| deserialize_value(value) }
|
236
|
+
initialize_with_keyword_args(**deserialized_fields)
|
237
|
+
end
|
238
|
+
|
206
239
|
# A thin wrapper around the private initialize method that accepts a field
|
207
240
|
# hash and refreshes the existing object.
|
208
241
|
#
|
@@ -218,7 +251,7 @@ module Familia
|
|
218
251
|
# @return [Array] The list of field names that were updated.
|
219
252
|
def optimistic_refresh(**fields)
|
220
253
|
Familia.ld "[optimistic_refresh] #{self.class} #{rediskey} #{fields.keys}"
|
221
|
-
|
254
|
+
initialize_with_keyword_args_from_redis(**fields)
|
222
255
|
end
|
223
256
|
|
224
257
|
# Determines the unique identifier for the instance
|
@@ -247,6 +280,19 @@ module Familia
|
|
247
280
|
|
248
281
|
unique_id
|
249
282
|
end
|
283
|
+
|
284
|
+
# The principle is: **If Familia objects have `to_s`, then they should work
|
285
|
+
# everywhere strings are expected**, including as Redis hash field names.
|
286
|
+
def to_s
|
287
|
+
# Enable polymorphic string usage for Familia objects
|
288
|
+
# This allows passing Familia objects directly where strings are expected
|
289
|
+
# without requiring explicit .identifier calls
|
290
|
+
identifier.to_s
|
291
|
+
rescue => e
|
292
|
+
# Fallback for cases where identifier might fail
|
293
|
+
Familia.ld "[#{self.class}#to_s] Failed to get identifier: #{e.message}"
|
294
|
+
"#<#{self.class}:0x#{object_id.to_s(16)}>"
|
295
|
+
end
|
250
296
|
end
|
251
297
|
end
|
252
298
|
|
@@ -16,7 +16,7 @@ module Familia
|
|
16
16
|
# +return+ [Integer] Returns 1 if the field is new and added, 0 if the
|
17
17
|
# field already existed and the value was updated.
|
18
18
|
def []=(field, val)
|
19
|
-
ret = redis.hset rediskey, field, serialize_value(val)
|
19
|
+
ret = redis.hset rediskey, field.to_s, serialize_value(val)
|
20
20
|
update_expiration
|
21
21
|
ret
|
22
22
|
rescue TypeError => e
|
@@ -31,12 +31,12 @@ module Familia
|
|
31
31
|
alias store []=
|
32
32
|
|
33
33
|
def [](field)
|
34
|
-
deserialize_value redis.hget(rediskey, field)
|
34
|
+
deserialize_value redis.hget(rediskey, field.to_s)
|
35
35
|
end
|
36
36
|
alias get []
|
37
37
|
|
38
38
|
def fetch(field, default = nil)
|
39
|
-
ret = self[field]
|
39
|
+
ret = self[field.to_s]
|
40
40
|
if ret.nil?
|
41
41
|
raise IndexError, "No such index for: #{field}" if default.nil?
|
42
42
|
|
@@ -62,7 +62,7 @@ module Familia
|
|
62
62
|
alias all hgetall
|
63
63
|
|
64
64
|
def key?(field)
|
65
|
-
redis.hexists rediskey, field
|
65
|
+
redis.hexists rediskey, field.to_s
|
66
66
|
end
|
67
67
|
alias has_key? key?
|
68
68
|
alias include? key?
|
@@ -72,12 +72,12 @@ module Familia
|
|
72
72
|
# @param field [String] The field to remove
|
73
73
|
# @return [Integer] The number of fields that were removed (0 or 1)
|
74
74
|
def remove_field(field)
|
75
|
-
redis.hdel rediskey, field
|
75
|
+
redis.hdel rediskey, field.to_s
|
76
76
|
end
|
77
77
|
alias remove remove_field # deprecated
|
78
78
|
|
79
79
|
def increment(field, by = 1)
|
80
|
-
redis.hincrby(rediskey, field, by).to_i
|
80
|
+
redis.hincrby(rediskey, field.to_s, by).to_i
|
81
81
|
end
|
82
82
|
alias incr increment
|
83
83
|
alias incrby increment
|
@@ -100,7 +100,8 @@ module Familia
|
|
100
100
|
alias merge! update
|
101
101
|
|
102
102
|
def values_at *fields
|
103
|
-
|
103
|
+
string_fields = fields.flatten.compact.map(&:to_s)
|
104
|
+
elements = redis.hmget(rediskey, *string_fields)
|
104
105
|
deserialize_values(*elements)
|
105
106
|
end
|
106
107
|
|
data/lib/familia.rb
CHANGED
@@ -0,0 +1,159 @@
|
|
1
|
+
require_relative '../lib/familia'
|
2
|
+
require_relative './test_helpers'
|
3
|
+
|
4
|
+
Familia.debug = false
|
5
|
+
|
6
|
+
@identifier = 'tryouts-28@onetimesecret.com'
|
7
|
+
@customer = Customer.new @identifier
|
8
|
+
|
9
|
+
## Basic save functionality works
|
10
|
+
@customer.name = 'John Doe'
|
11
|
+
@customer.save
|
12
|
+
#=> true
|
13
|
+
|
14
|
+
## to_h returns field hash with all Customer fields
|
15
|
+
@customer.to_h.class
|
16
|
+
#=> Hash
|
17
|
+
|
18
|
+
## to_h includes the fields we set (using symbol keys)
|
19
|
+
@customer.to_h[:name]
|
20
|
+
#=> "John Doe"
|
21
|
+
|
22
|
+
## to_h includes the key field (using symbol keys)
|
23
|
+
@customer.to_h[:key]
|
24
|
+
#=> "tryouts-28@onetimesecret.com"
|
25
|
+
|
26
|
+
## to_a returns field array in definition order
|
27
|
+
@customer.to_a.class
|
28
|
+
#=> Array
|
29
|
+
|
30
|
+
## to_a includes values in field order (name should be at index 5)
|
31
|
+
@customer.to_a[5]
|
32
|
+
#=> "John Doe"
|
33
|
+
|
34
|
+
## batch_update can update multiple fields atomically, to_h
|
35
|
+
@result = @customer.batch_update(name: 'Jane Smith', email: 'jane@example.com')
|
36
|
+
@result.to_h
|
37
|
+
#=> {:success=>true, :results=>[0, 0]}
|
38
|
+
|
39
|
+
## batch_update returns successful result, successful?
|
40
|
+
@result.successful?
|
41
|
+
#=> true
|
42
|
+
|
43
|
+
## batch_update returns successful result, tuple
|
44
|
+
@result.tuple
|
45
|
+
#=> [true, [0, 0]]
|
46
|
+
|
47
|
+
## batch_update returns successful result, to_a
|
48
|
+
@result.to_a
|
49
|
+
#=> [true, [0, 0]]
|
50
|
+
|
51
|
+
## batch_update updates object fields in memory, confirm fields changed
|
52
|
+
[@customer.name, @customer.email]
|
53
|
+
#=> ["Jane Smith", "jane@example.com"]
|
54
|
+
|
55
|
+
## batch_update persists to Redis
|
56
|
+
@customer.refresh!
|
57
|
+
[@customer.name, @customer.email]
|
58
|
+
#=> ["Jane Smith", "jane@example.com"]
|
59
|
+
|
60
|
+
## batch_update with update_expiration: false works
|
61
|
+
@customer.batch_update(name: 'Bob Jones', update_expiration: false)
|
62
|
+
@customer.refresh!
|
63
|
+
@customer.name
|
64
|
+
#=> "Bob Jones"
|
65
|
+
|
66
|
+
## apply_fields updates object in memory only (1 of 2)
|
67
|
+
@customer.apply_fields(name: 'Memory Only', email: 'memory@test.com')
|
68
|
+
[@customer.name, @customer.email]
|
69
|
+
#=> ["Memory Only", "memory@test.com"]
|
70
|
+
|
71
|
+
## apply_fields doesn't persist to Redis (2 of 2)
|
72
|
+
@customer.refresh!
|
73
|
+
[@customer.name, @customer.email]
|
74
|
+
#=> ["Bob Jones", "jane@example.com"]
|
75
|
+
|
76
|
+
## serialize_value handles strings
|
77
|
+
@customer.serialize_value("test string")
|
78
|
+
#=> "test string"
|
79
|
+
|
80
|
+
## serialize_value handles numbers
|
81
|
+
@customer.serialize_value(42)
|
82
|
+
#=> "42"
|
83
|
+
|
84
|
+
## serialize_value handles hashes as JSON
|
85
|
+
@customer.serialize_value({key: 'value', num: 123})
|
86
|
+
#=> "{\"key\":\"value\",\"num\":123}"
|
87
|
+
|
88
|
+
## serialize_value handles arrays as JSON
|
89
|
+
@customer.serialize_value([1, 2, 'three'])
|
90
|
+
#=> "[1,2,\"three\"]"
|
91
|
+
|
92
|
+
## deserialize_value handles JSON strings back to objects
|
93
|
+
@customer.deserialize_value('{"key":"value","num":123}')
|
94
|
+
#=> {:key=>"value", :num=>123}
|
95
|
+
|
96
|
+
## deserialize_value handles JSON arrays
|
97
|
+
@customer.deserialize_value('[1,2,"three"]')
|
98
|
+
#=> [1, 2, "three"]
|
99
|
+
|
100
|
+
## deserialize_value handles plain strings
|
101
|
+
@customer.deserialize_value('plain string')
|
102
|
+
#=> "plain string"
|
103
|
+
|
104
|
+
## transaction method works with block
|
105
|
+
result = @customer.transaction do |conn|
|
106
|
+
conn.hset @customer.rediskey, 'temp_field', 'temp_value'
|
107
|
+
conn.hset @customer.rediskey, 'another_field', 'another_value'
|
108
|
+
end
|
109
|
+
result.size
|
110
|
+
#=> 2
|
111
|
+
|
112
|
+
## refresh! reloads from Redis
|
113
|
+
@customer.refresh!
|
114
|
+
@customer.hget('temp_field')
|
115
|
+
#=> "temp_value"
|
116
|
+
|
117
|
+
## Empty batch_update still works
|
118
|
+
result = @customer.batch_update()
|
119
|
+
result.successful?
|
120
|
+
#=> true
|
121
|
+
|
122
|
+
## destroy! removes object from Redis (1 of 2)
|
123
|
+
@customer.destroy!
|
124
|
+
#=> true
|
125
|
+
|
126
|
+
## After destroy!, Redis key no longer exists (2 of 2)
|
127
|
+
@customer.exists?
|
128
|
+
#=> false
|
129
|
+
|
130
|
+
## destroy! removes object from Redis, not the in-memory object (2 of 2)
|
131
|
+
@customer.refresh!
|
132
|
+
@customer.name
|
133
|
+
#=> "Bob Jones"
|
134
|
+
|
135
|
+
## clear_fields! removes the in-memory object fields
|
136
|
+
@customer.clear_fields!
|
137
|
+
@customer.name
|
138
|
+
#=> nil
|
139
|
+
|
140
|
+
## Fresh customer for testing new field creation
|
141
|
+
@fresh_customer = Customer.new 'fresh-customer@test.com'
|
142
|
+
@fresh_customer.class
|
143
|
+
#=> Customer
|
144
|
+
|
145
|
+
## batch_update with new fields returns [1, 1] for new field creation
|
146
|
+
@fresh_customer.remove_field('role')
|
147
|
+
@fresh_customer.remove_field('planid')
|
148
|
+
@fresh_result = @fresh_customer.batch_update(role: 'admin', planid: 'premium')
|
149
|
+
@fresh_result.to_h
|
150
|
+
#=> {:success=>true, :results=>[1, 1]}
|
151
|
+
|
152
|
+
## Fresh customer fields are set correctly
|
153
|
+
[@fresh_customer.role, @fresh_customer.planid]
|
154
|
+
#=> ["admin", "premium"]
|
155
|
+
|
156
|
+
## Fresh customer changes persist to Redis
|
157
|
+
@fresh_customer.refresh!
|
158
|
+
[@fresh_customer.role, @fresh_customer.planid]
|
159
|
+
#=> ["admin", "premium"]
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative '../lib/familia'
|
2
|
+
require_relative './test_helpers'
|
3
|
+
|
4
|
+
Familia.debug = false
|
5
|
+
|
6
|
+
## Existing positional argument initialization still works
|
7
|
+
@customer1 = Customer.new 'tryouts-29@test.com', '', '', '', '', 'John Doe'
|
8
|
+
[@customer1.custid, @customer1.name]
|
9
|
+
#=> ["tryouts-29@test.com", "John Doe"]
|
10
|
+
|
11
|
+
## Keyword argument initialization works (order independent)
|
12
|
+
@customer2 = Customer.new(name: 'Jane Smith', custid: 'jane@test.com', email: 'jane@example.com')
|
13
|
+
[@customer2.custid, @customer2.name, @customer2.email]
|
14
|
+
#=> ["jane@test.com", "Jane Smith", "jane@example.com"]
|
15
|
+
|
16
|
+
## Keyword arguments are order independent (different order, same result)
|
17
|
+
@customer3 = Customer.new(email: 'bob@example.com', custid: 'bob@test.com', name: 'Bob Jones')
|
18
|
+
[@customer3.custid, @customer3.name, @customer3.email]
|
19
|
+
#=> ["bob@test.com", "Bob Jones", "bob@example.com"]
|
20
|
+
|
21
|
+
## Legacy hash support (single hash argument)
|
22
|
+
@customer4 = Customer.new({custid: 'legacy@test.com', name: 'Legacy User', role: 'admin'})
|
23
|
+
[@customer4.custid, @customer4.name, @customer4.role]
|
24
|
+
#=> ["legacy@test.com", "Legacy User", "admin"]
|
25
|
+
|
26
|
+
## Empty initialization works
|
27
|
+
@customer5 = Customer.new
|
28
|
+
@customer5.class
|
29
|
+
#=> Customer
|
30
|
+
|
31
|
+
## Keyword args with save and retrieval
|
32
|
+
@customer6 = Customer.new(custid: 'save-test@test.com', name: 'Save Test', email: 'save@example.com')
|
33
|
+
@customer6.save
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## Saved customer can be retrieved with correct values
|
37
|
+
@customer6.refresh!
|
38
|
+
[@customer6.custid, @customer6.name, @customer6.email]
|
39
|
+
#=> ["save-test@test.com", "Save Test", "save@example.com"]
|
40
|
+
|
41
|
+
## Keyword initialization sets key field correctly
|
42
|
+
@customer6.key
|
43
|
+
#=> "save-test@test.com"
|
44
|
+
|
45
|
+
## Mixed valid and nil values in keyword args (nil values stay nil)
|
46
|
+
@customer7 = Customer.new(custid: 'mixed@test.com', name: 'Mixed Test', email: nil, role: 'user')
|
47
|
+
[@customer7.custid, @customer7.name, @customer7.email, @customer7.role]
|
48
|
+
#=> ["mixed@test.com", "Mixed Test", nil, "user"]
|
49
|
+
|
50
|
+
## to_h works correctly with keyword-initialized objects
|
51
|
+
@customer2.to_h[:name]
|
52
|
+
#=> "Jane Smith"
|
53
|
+
|
54
|
+
## to_a works correctly with keyword-initialized objects
|
55
|
+
@customer2.to_a[5] # name field should be at index 5
|
56
|
+
#=> "Jane Smith"
|
57
|
+
|
58
|
+
## Session has limited fields (only sessid defined)
|
59
|
+
@session1 = Session.new('sess123')
|
60
|
+
@session1.sessid
|
61
|
+
#=> "sess123"
|
62
|
+
|
63
|
+
## Session with keyword args works
|
64
|
+
@session2 = Session.new(sessid: 'keyword-sess')
|
65
|
+
@session2.sessid
|
66
|
+
#=> "keyword-sess"
|
67
|
+
|
68
|
+
## Session with legacy hash
|
69
|
+
@session3 = Session.new({sessid: 'hash-sess'})
|
70
|
+
@session3.sessid
|
71
|
+
#=> "hash-sess"
|
72
|
+
|
73
|
+
## CustomDomain with keyword initialization
|
74
|
+
@domain1 = CustomDomain.new(display_domain: 'api.example.com', custid: 'domain-test@test.com')
|
75
|
+
[@domain1.display_domain, @domain1.custid]
|
76
|
+
#=> ["api.example.com", "domain-test@test.com"]
|
77
|
+
|
78
|
+
## CustomDomain still works with positional args
|
79
|
+
@domain2 = CustomDomain.new('web.example.com', 'positional@test.com')
|
80
|
+
[@domain2.display_domain, @domain2.custid]
|
81
|
+
#=> ["web.example.com", "positional@test.com"]
|
82
|
+
|
83
|
+
## Keyword initialization can skip fields (they remain nil/empty)
|
84
|
+
@partial = Customer.new(custid: 'partial@test.com', name: 'Partial User')
|
85
|
+
[@partial.custid, @partial.name, @partial.email]
|
86
|
+
#=> ["partial@test.com", "Partial User", nil]
|
87
|
+
|
88
|
+
## Complex initialization with save/refresh cycle
|
89
|
+
@complex = Customer.new(
|
90
|
+
custid: 'complex@test.com',
|
91
|
+
name: 'Complex User',
|
92
|
+
email: 'complex@example.com',
|
93
|
+
role: 'admin',
|
94
|
+
verified: true
|
95
|
+
)
|
96
|
+
@complex.save
|
97
|
+
@complex.refresh!
|
98
|
+
[@complex.custid, @complex.name, @complex.role, @complex.verified]
|
99
|
+
#=> ["complex@test.com", "Complex User", "admin", "true"]
|
100
|
+
|
101
|
+
## Clean up saved test objects
|
102
|
+
[@customer6, @complex].map(&:delete!)
|
103
|
+
#=> [true, true]
|
104
|
+
|
105
|
+
## "Cleaning up" test objects that were never saved returns false.
|
106
|
+
@customer1.save
|
107
|
+
ret = [
|
108
|
+
@customer1, @customer2, @customer3, @customer4, @customer6, @customer7,
|
109
|
+
@session1, @session2, @session3,
|
110
|
+
@domain1, @domain2,
|
111
|
+
@partial, @complex
|
112
|
+
].map(&:destroy!)
|
113
|
+
#=> [true, false, false, false, false, false, false, false, false, false, false, false, false]
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# try/91_json_bug_try.rb
|
2
|
+
|
3
|
+
require_relative '../lib/familia'
|
4
|
+
require_relative './test_helpers'
|
5
|
+
|
6
|
+
Familia.debug = false
|
7
|
+
|
8
|
+
# Define a simple model with fields that should handle JSON data
|
9
|
+
class JsonTest < Familia::Horreum
|
10
|
+
identifier :id
|
11
|
+
field :id
|
12
|
+
field :config # This should be able to store Hash objects
|
13
|
+
field :tags # This should be able to store Array objects
|
14
|
+
field :simple # This should store simple strings as-is
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create an instance with JSON data
|
18
|
+
@test_obj = JsonTest.new
|
19
|
+
@test_obj.id = "json_test_1"
|
20
|
+
|
21
|
+
## Test 1: Store a Hash - should serialize to JSON automatically
|
22
|
+
@test_obj.config = { theme: "dark", notifications: true, settings: { volume: 80 } }
|
23
|
+
@test_obj.config.class
|
24
|
+
#=> Hash
|
25
|
+
|
26
|
+
## Test 2: Store an Array - should serialize to JSON automatically
|
27
|
+
@test_obj.tags = ["ruby", "redis", "json", "familia"]
|
28
|
+
@test_obj.tags.class
|
29
|
+
#=> Array
|
30
|
+
|
31
|
+
## Test 3: Store a simple string - should remain as string
|
32
|
+
@test_obj.simple = "just a string"
|
33
|
+
@test_obj.simple.class
|
34
|
+
#=> String
|
35
|
+
|
36
|
+
## Save the object - this should call serialize_value and use to_json
|
37
|
+
@test_obj.save
|
38
|
+
#=> true
|
39
|
+
|
40
|
+
## Verify what's actually stored in Redis (raw)
|
41
|
+
raw_data = @test_obj.hgetall
|
42
|
+
p [:plop, @test_obj]
|
43
|
+
puts "Raw Redis data:"
|
44
|
+
raw_data
|
45
|
+
#=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"redis\",\"json\",\"familia\"]", "simple"=>"just a string", "key"=>"json_test_1"}
|
46
|
+
|
47
|
+
## BUG: After refresh, JSON data comes back as strings instead of parsed objects
|
48
|
+
@test_obj.refresh!
|
49
|
+
|
50
|
+
## Test 4: Hash should be deserialized back to Hash
|
51
|
+
puts "Config after refresh:"
|
52
|
+
puts @test_obj.config.inspect
|
53
|
+
puts "Config class: "
|
54
|
+
[@test_obj.config.class, @test_obj.config.inspect]
|
55
|
+
#=> [Hash, "{:theme=>\"dark\", :notifications=>true, :settings=>{:volume=>80}}"]
|
56
|
+
|
57
|
+
## Test 5: Array should be deserialized back to Array
|
58
|
+
puts "Tags after refresh:"
|
59
|
+
puts @test_obj.tags.inspect
|
60
|
+
puts "Tags class: #{@test_obj.tags.class}"
|
61
|
+
@test_obj.tags.inspect
|
62
|
+
@test_obj.tags.class
|
63
|
+
#=> ["ruby", "redis", "json", "familia"]
|
64
|
+
#=> Array
|
65
|
+
|
66
|
+
## Test 6: Simple string should remain a string (this works correctly)
|
67
|
+
puts "Simple after refresh:"
|
68
|
+
puts @test_obj.simple.inspect
|
69
|
+
puts "Simple class: #{@test_obj.simple.class}"
|
70
|
+
@test_obj.simple.inspect
|
71
|
+
@test_obj.simple.class
|
72
|
+
#=> "just a string"
|
73
|
+
#=> String
|
74
|
+
|
75
|
+
## Demonstrate the asymmetry:
|
76
|
+
puts "\n=== ASYMMETRY DEMONSTRATION ==="
|
77
|
+
puts "Before save: config is #{@test_obj.config.class}"
|
78
|
+
@test_obj.config = { example: "data" }
|
79
|
+
puts "After assignment: config is #{@test_obj.config.class}"
|
80
|
+
@test_obj.save
|
81
|
+
puts "After save: config is still #{@test_obj.config.class}"
|
82
|
+
@test_obj.refresh!
|
83
|
+
puts "After refresh: config is now #{@test_obj.config.class}!"
|
84
|
+
|
85
|
+
## Clean up
|
86
|
+
@test_obj.destroy!
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require_relative '../lib/familia'
|
2
|
+
require_relative './test_helpers'
|
3
|
+
|
4
|
+
Familia.debug = false
|
5
|
+
|
6
|
+
# Test the updated deserialize_value method
|
7
|
+
class SymbolizeTest < Familia::Horreum
|
8
|
+
identifier :id
|
9
|
+
field :id
|
10
|
+
field :config
|
11
|
+
end
|
12
|
+
|
13
|
+
@test_obj = SymbolizeTest.new
|
14
|
+
@test_obj.id = "symbolize_test_1"
|
15
|
+
|
16
|
+
## Test with a hash containing string keys
|
17
|
+
@test_hash = { "name" => "John", "age" => 30, "nested" => { "theme" => "dark" } }
|
18
|
+
@test_obj.config = @test_hash
|
19
|
+
@test_obj.save
|
20
|
+
|
21
|
+
## Original hash has string keys
|
22
|
+
@test_hash.keys
|
23
|
+
#=> ["name", "age", "nested"]
|
24
|
+
|
25
|
+
## After save and refresh, default behavior uses symbol keys
|
26
|
+
@test_obj.refresh!
|
27
|
+
@test_obj.config.keys
|
28
|
+
#=> [:name, :age, :nested]
|
29
|
+
|
30
|
+
## Nested hash also has symbol keys
|
31
|
+
@test_obj.config[:nested].keys
|
32
|
+
#=> [:theme]
|
33
|
+
|
34
|
+
## Get raw JSON from Redis
|
35
|
+
@raw_json = @test_obj.hget('config')
|
36
|
+
@raw_json.class
|
37
|
+
#=> String
|
38
|
+
|
39
|
+
## deserialize_value with default symbolize: true returns symbol keys
|
40
|
+
@symbol_result = @test_obj.deserialize_value(@raw_json)
|
41
|
+
@symbol_result.keys
|
42
|
+
#=> [:name, :age, :nested]
|
43
|
+
|
44
|
+
## Nested hash in symbol result also has symbol keys
|
45
|
+
@symbol_result[:nested].keys
|
46
|
+
#=> [:theme]
|
47
|
+
|
48
|
+
## deserialize_value with symbolize: false returns string keys
|
49
|
+
@string_result = @test_obj.deserialize_value(@raw_json, symbolize: false)
|
50
|
+
@string_result.keys
|
51
|
+
#=> ["name", "age", "nested"]
|
52
|
+
|
53
|
+
## Nested hash in string result also has string keys
|
54
|
+
@string_result["nested"].keys
|
55
|
+
#=> ["theme"]
|
56
|
+
|
57
|
+
## Values are preserved correctly in both cases
|
58
|
+
@symbol_result[:name]
|
59
|
+
#=> "John"
|
60
|
+
|
61
|
+
@string_result["name"]
|
62
|
+
#=> "John"
|
63
|
+
|
64
|
+
## Arrays are handled correctly too
|
65
|
+
@test_obj.config = [{ "item" => "value" }, "string", 123]
|
66
|
+
@test_obj.save
|
67
|
+
@array_json = @test_obj.hget('config')
|
68
|
+
#=> "[{\"item\":\"value\"},\"string\",123]"
|
69
|
+
|
70
|
+
## Array with symbolize: true converts hash keys to symbols
|
71
|
+
@symbol_array = @test_obj.deserialize_value(@array_json)
|
72
|
+
@symbol_array[0].keys
|
73
|
+
#=> [:item]
|
74
|
+
|
75
|
+
## Array with symbolize: false keeps hash keys as strings
|
76
|
+
@string_array = @test_obj.deserialize_value(@array_json, symbolize: false)
|
77
|
+
@string_array[0].keys
|
78
|
+
#=> ["item"]
|
79
|
+
|
80
|
+
## Non-hash/array values are returned as-is
|
81
|
+
@test_obj.deserialize_value('"just a string"')
|
82
|
+
#=> "\"just a string\""
|
83
|
+
|
84
|
+
## Non-hash/array values are returned as-is
|
85
|
+
@test_obj.deserialize_value('just a string')
|
86
|
+
#=> "just a string"
|
87
|
+
|
88
|
+
@test_obj.deserialize_value('42')
|
89
|
+
#=> "42"
|
90
|
+
|
91
|
+
## Invalid JSON returns original string
|
92
|
+
@test_obj.deserialize_value('invalid json')
|
93
|
+
#=> "invalid json"
|
94
|
+
|
95
|
+
## Clean up
|
96
|
+
@test_obj.destroy!
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# try/93_string_coercion_try.rb
|
2
|
+
|
3
|
+
require_relative '../lib/familia'
|
4
|
+
require_relative './test_helpers'
|
5
|
+
|
6
|
+
Familia.debug = false
|
7
|
+
|
8
|
+
# Error handling: object without proper identifier setup
|
9
|
+
class ::BadIdentifierTest < Familia::Horreum
|
10
|
+
# No identifier method defined - should cause issues
|
11
|
+
end
|
12
|
+
|
13
|
+
@bad_obj = ::BadIdentifierTest.new
|
14
|
+
|
15
|
+
# Test polymorphic string usage for Familia objects
|
16
|
+
@customer_id = 'customer-string-coercion-test'
|
17
|
+
@customer = Customer.new(@customer_id)
|
18
|
+
@customer.name = 'John Doe'
|
19
|
+
@customer.planid = 'premium'
|
20
|
+
@customer.save
|
21
|
+
|
22
|
+
@session_id = 'session-string-coercion-test'
|
23
|
+
@session = Session.new(@session_id)
|
24
|
+
@session.custid = @customer_id
|
25
|
+
@session.useragent = 'Test Browser'
|
26
|
+
@session.save
|
27
|
+
|
28
|
+
## Complex identifier test with array-based identifier
|
29
|
+
@bone = Bone.new
|
30
|
+
@bone.token = 'test_token'
|
31
|
+
@bone.name = 'test_name'
|
32
|
+
|
33
|
+
|
34
|
+
## Basic to_s functionality returns identifier
|
35
|
+
@customer.to_s
|
36
|
+
#=> @customer_id
|
37
|
+
|
38
|
+
## String interpolation works seamlessly
|
39
|
+
"Customer: #{@customer}"
|
40
|
+
#=> "Customer: #{@customer_id}"
|
41
|
+
|
42
|
+
## Explicit identifier call returns same value
|
43
|
+
@customer.to_s == @customer.identifier
|
44
|
+
#=> true
|
45
|
+
|
46
|
+
## Session to_s works with generated identifier
|
47
|
+
@session.to_s
|
48
|
+
#=> @session_id
|
49
|
+
|
50
|
+
## Method accepting string parameter works with Familia object
|
51
|
+
def lookup_by_id(id_string)
|
52
|
+
id_string.to_s.upcase
|
53
|
+
end
|
54
|
+
|
55
|
+
lookup_by_id(@customer)
|
56
|
+
#=> @customer_id.upcase
|
57
|
+
|
58
|
+
## Hash key assignment using Familia object (implicit string conversion)
|
59
|
+
@data = {}
|
60
|
+
@data[@customer] = 'customer_data'
|
61
|
+
@data[@customer]
|
62
|
+
#=> 'customer_data'
|
63
|
+
|
64
|
+
## Array operations work with mixed types
|
65
|
+
@mixed_array = [@customer_id, @customer, @session]
|
66
|
+
@mixed_array.map(&:to_s)
|
67
|
+
#=> [@customer_id, @customer_id, @session_id]
|
68
|
+
|
69
|
+
## String comparison works
|
70
|
+
@customer.to_s == @customer_id
|
71
|
+
#=> true
|
72
|
+
|
73
|
+
## Join operations work seamlessly
|
74
|
+
[@customer, 'separator', @session].join(':')
|
75
|
+
#=> "#{@customer_id}:separator:#{@session_id}"
|
76
|
+
|
77
|
+
## Case statement works with string matching
|
78
|
+
def classify_id(obj)
|
79
|
+
case obj.to_s
|
80
|
+
when /customer/
|
81
|
+
'customer_type'
|
82
|
+
when /session/
|
83
|
+
'session_type'
|
84
|
+
else
|
85
|
+
'unknown_type'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
classify_id(@customer)
|
90
|
+
#=> 'customer_type'
|
91
|
+
|
92
|
+
classify_id(@session)
|
93
|
+
#=> 'session_type'
|
94
|
+
|
95
|
+
## Polymorphic method accepting both strings and Familia objects
|
96
|
+
def process_identifier(id_or_object)
|
97
|
+
# Can handle both string IDs and Familia objects uniformly
|
98
|
+
processed_id = id_or_object.to_s
|
99
|
+
"processed:#{processed_id}"
|
100
|
+
end
|
101
|
+
|
102
|
+
process_identifier(@customer_id)
|
103
|
+
#=> "processed:#{@customer_id}"
|
104
|
+
|
105
|
+
process_identifier(@customer)
|
106
|
+
#=> "processed:#{@customer_id}"
|
107
|
+
|
108
|
+
## Redis storage using object as string key
|
109
|
+
@metadata = Familia::HashKey.new 'metadata'
|
110
|
+
@metadata[@customer] = 'customer_metadata'
|
111
|
+
@metadata[@customer.to_s] # Same key access
|
112
|
+
#=> 'customer_metadata'
|
113
|
+
|
114
|
+
## Cleanup after test
|
115
|
+
@metadata.delete!
|
116
|
+
#=> true
|
117
|
+
|
118
|
+
@customer.delete!
|
119
|
+
#=> true
|
120
|
+
|
121
|
+
@session.delete!
|
122
|
+
#=> true
|
123
|
+
|
124
|
+
## to_s handles identifier errors gracefully
|
125
|
+
@bad_obj.to_s.include?('BadIdentifierTest')
|
126
|
+
#=> true
|
127
|
+
|
128
|
+
## Array-based identifier works with to_s
|
129
|
+
@bone.to_s
|
130
|
+
#=> 'test_token:test_name'
|
131
|
+
|
132
|
+
## String operations on complex identifier
|
133
|
+
@bone.to_s.split(':')
|
134
|
+
#=> ['test_token', 'test_name']
|
135
|
+
|
136
|
+
## Cleanup a key that does not exist
|
137
|
+
@bone.delete!
|
138
|
+
#=> false
|
139
|
+
|
140
|
+
## Cleanup a key that exists
|
141
|
+
@bone.save
|
142
|
+
@bone.delete!
|
143
|
+
#=> true
|
144
|
+
|
145
|
+
## Performance consideration: to_s caching behavior
|
146
|
+
@customer2 = Customer.new('performance-test')
|
147
|
+
@first_call = @customer2.to_s
|
148
|
+
@second_call = @customer2.to_s
|
149
|
+
@first_call == @second_call
|
150
|
+
#=> true
|
151
|
+
|
152
|
+
## Delete customer2
|
153
|
+
[@customer2.exists?, @customer2.delete!]
|
154
|
+
#=> [false, false]
|
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.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-05-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -117,10 +117,15 @@ files:
|
|
117
117
|
- try/25_redis_type_hash_try.rb
|
118
118
|
- try/26_redis_bool_try.rb
|
119
119
|
- try/27_redis_horreum_try.rb
|
120
|
+
- try/28_redis_horreum_serialization_try.rb
|
121
|
+
- try/29_redis_horreum_initialization_try.rb
|
120
122
|
- try/30_familia_object_try.rb
|
121
123
|
- try/35_feature_safedump_try.rb
|
122
124
|
- try/40_customer_try.rb
|
123
125
|
- try/41_customer_safedump_try.rb
|
126
|
+
- try/91_json_bug_try.rb
|
127
|
+
- try/92_symbolize_try.rb
|
128
|
+
- try/93_string_coercion_try.rb
|
124
129
|
- try/test_helpers.rb
|
125
130
|
homepage: https://github.com/delano/familia
|
126
131
|
licenses:
|