familia 1.2.0 → 2.0.0.pre.pre
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/.github/workflows/ci.yml +68 -0
- data/.github/workflows/docs.yml +64 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +3 -1
- data/.rubocop.yml +16 -9
- data/.rubocop_todo.yml +177 -31
- data/.yardopts +9 -0
- data/CLAUDE.md +141 -0
- data/Gemfile +15 -2
- data/Gemfile.lock +76 -34
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +317 -0
- data/familia.gemspec +9 -5
- data/lib/familia/base.rb +19 -9
- data/lib/familia/connection.rb +232 -65
- data/lib/familia/core_ext.rb +1 -1
- data/lib/familia/datatype/commands.rb +59 -0
- data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
- data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
- data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
- data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
- data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
- data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
- data/lib/familia/datatype.rb +243 -0
- data/lib/familia/errors.rb +5 -2
- data/lib/familia/features/expiration.rb +33 -34
- data/lib/familia/features/quantization.rb +9 -3
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features.rb +2 -2
- data/lib/familia/horreum/class_methods.rb +97 -110
- data/lib/familia/horreum/commands.rb +46 -51
- data/lib/familia/horreum/connection.rb +82 -0
- data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
- data/lib/familia/horreum/serialization.rb +61 -198
- data/lib/familia/horreum/settings.rb +6 -17
- data/lib/familia/horreum/utils.rb +11 -10
- data/lib/familia/horreum.rb +69 -60
- data/lib/familia/logging.rb +12 -12
- data/lib/familia/multi_result.rb +72 -0
- data/lib/familia/refinements.rb +7 -44
- data/lib/familia/settings.rb +11 -11
- data/lib/familia/utils.rb +123 -90
- data/lib/familia/version.rb +4 -21
- data/lib/familia.rb +17 -12
- data/lib/middleware/database_middleware.rb +150 -0
- data/try/configuration/scenarios_try.rb +65 -0
- data/try/core/connection_try.rb +58 -0
- data/try/core/errors_try.rb +93 -0
- data/try/core/extensions_try.rb +26 -0
- data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
- data/try/{00_familia_try.rb → core/familia_try.rb} +5 -3
- data/try/core/middleware_try.rb +68 -0
- data/try/core/refinements_try.rb +39 -0
- data/try/core/settings_try.rb +76 -0
- data/try/core/tools_try.rb +54 -0
- data/try/core/utils_try.rb +189 -0
- data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
- data/try/datatypes/datatype_base_try.rb +69 -0
- data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
- data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
- data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
- data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
- data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
- data/try/edge_cases/empty_identifiers_try.rb +48 -0
- data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
- data/try/edge_cases/json_serialization_try.rb +85 -0
- data/try/edge_cases/race_conditions_try.rb +60 -0
- data/try/edge_cases/reserved_keywords_try.rb +59 -0
- data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
- data/try/edge_cases/ttl_side_effects_try.rb +51 -0
- data/try/features/expiration_try.rb +86 -0
- data/try/features/quantization_try.rb +90 -0
- data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
- data/try/features/safe_dump_try.rb +137 -0
- data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
- data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
- data/try/horreum/class_methods_try.rb +41 -0
- data/try/horreum/commands_try.rb +49 -0
- data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
- data/try/horreum/relations_try.rb +146 -0
- data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
- data/try/horreum/settings_try.rb +43 -0
- data/try/integration/cross_component_try.rb +46 -0
- data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
- data/try/{40_customer_try.rb → models/customer_try.rb} +20 -17
- data/try/models/datatype_base_try.rb +101 -0
- data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
- data/try/performance/benchmarks_try.rb +55 -0
- data/try/pooling/README.md +20 -0
- data/try/pooling/configurable_stress_test_try.rb +435 -0
- data/try/pooling/connection_pool_test_try.rb +273 -0
- data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- data/try/pooling/lib/connection_pool_metrics.rb +372 -0
- data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
- data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
- data/try/pooling/lib/visualize_stress_results.rb +434 -0
- data/try/pooling/pool_siege_try.rb +509 -0
- data/try/pooling/run_stress_tests_try.rb +482 -0
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
- data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
- data/try/prototypes/atomic_saves_v4.rb +105 -0
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- metadata +140 -43
- data/.github/workflows/ruby.yml +0 -71
- data/VERSION.yml +0 -4
- data/lib/familia/redistype/commands.rb +0 -59
- data/lib/familia/redistype.rb +0 -228
- data/lib/familia/tools.rb +0 -68
- data/lib/redis_middleware.rb +0 -109
- data/try/20_redis_type_try.rb +0 -70
- data/try/91_json_bug_try.rb +0 -86
@@ -1,7 +1,9 @@
|
|
1
|
-
|
2
|
-
require_relative './test_helpers'
|
1
|
+
# try/datatypes/hash_try.rb
|
3
2
|
|
4
|
-
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/test_helpers'
|
5
|
+
|
6
|
+
@a = Bone.new 'atoken'
|
5
7
|
|
6
8
|
## Familia::HashKey#has_key? knows when there's no key
|
7
9
|
@a.props.has_key? 'fieldA'
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# try/datatypes/list_try.rb
|
1
2
|
|
2
|
-
require_relative '../lib/familia'
|
3
|
-
require_relative './test_helpers'
|
4
3
|
|
5
|
-
|
4
|
+
require_relative '../../lib/familia'
|
5
|
+
require_relative '../helpers/test_helpers'
|
6
|
+
|
7
|
+
@a = Bone.new 'atoken'
|
6
8
|
|
7
9
|
## Familia::List#push
|
8
10
|
ret = @a.owners.push :value1
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# try/datatypes/set_try.rb
|
1
2
|
|
2
|
-
require_relative '../lib/familia'
|
3
|
-
require_relative './test_helpers'
|
4
3
|
|
5
|
-
|
4
|
+
require_relative '../../lib/familia'
|
5
|
+
require_relative '../helpers/test_helpers'
|
6
|
+
|
7
|
+
@a = Bone.new 'atoken'
|
6
8
|
|
7
9
|
## Familia::Set#add
|
8
10
|
ret = @a.tags.add :a
|
@@ -1,11 +1,13 @@
|
|
1
|
-
|
2
|
-
require_relative './test_helpers'
|
1
|
+
# try/datatypes/sorted_set_try.rb
|
3
2
|
|
4
|
-
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/test_helpers'
|
5
5
|
|
6
|
-
|
6
|
+
|
7
|
+
@a = Bone.new 'atoken'
|
7
8
|
|
8
9
|
## Familia::SortedSet#add
|
10
|
+
@a = Bone.new 'atoken'
|
9
11
|
@a.metrics.add 2, :metric2
|
10
12
|
@a.metrics.add 4, :metric4
|
11
13
|
@a.metrics.add 0, :metric0
|
@@ -1,13 +1,13 @@
|
|
1
|
-
|
2
|
-
require_relative './test_helpers'
|
1
|
+
# try/datatypes/string_try.rb
|
3
2
|
|
4
|
-
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/test_helpers'
|
5
5
|
|
6
|
-
@a = Bone.new 'atoken2'
|
6
|
+
@a = Bone.new(token: 'atoken2')
|
7
7
|
|
8
|
-
## Bone#
|
9
|
-
@a.
|
10
|
-
#=> 'bone:atoken2:
|
8
|
+
## Bone#dbkey
|
9
|
+
@a.dbkey
|
10
|
+
#=> 'bone:atoken2:object'
|
11
11
|
|
12
12
|
## Familia::String#value should give default value
|
13
13
|
@a.value.value
|
@@ -27,7 +27,7 @@ require_relative './test_helpers'
|
|
27
27
|
|
28
28
|
## Familia::String.new
|
29
29
|
@ret = Familia::String.new 'arbitrary:key'
|
30
|
-
@ret.
|
30
|
+
@ret.dbkey
|
31
31
|
#=> 'arbitrary:key'
|
32
32
|
|
33
33
|
## instance set
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
# Test empty identifier edge cases
|
4
|
+
group "Empty Identifier Edge Cases"
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@user_class = Class.new(Familia::Horreum) do
|
8
|
+
identifier :email
|
9
|
+
field :name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
try "empty string identifier causes stack overflow" do
|
14
|
+
user = @user_class.new(email: "", name: "Test")
|
15
|
+
|
16
|
+
begin
|
17
|
+
user.exists? # This should cause infinite loop
|
18
|
+
false
|
19
|
+
rescue SystemStackError
|
20
|
+
true # Expected stack overflow
|
21
|
+
rescue => e
|
22
|
+
false # Unexpected error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
try "nil identifier causes stack overflow" do
|
27
|
+
user = @user_class.new(email: nil, name: "Test")
|
28
|
+
|
29
|
+
begin
|
30
|
+
user.exists?
|
31
|
+
false
|
32
|
+
rescue SystemStackError, Familia::NoIdentifier
|
33
|
+
true # Expected error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
try "validation workaround prevents stack overflow" do
|
38
|
+
user = @user_class.new(email: "", name: "Test")
|
39
|
+
|
40
|
+
# Workaround: validate before operations
|
41
|
+
if user.identifier.to_s.empty?
|
42
|
+
raise ArgumentError, "Empty identifier"
|
43
|
+
end
|
44
|
+
|
45
|
+
false # Should not reach here
|
46
|
+
rescue ArgumentError => e
|
47
|
+
e.message.include?("Empty identifier")
|
48
|
+
end
|
@@ -1,20 +1,24 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# try/edge_cases/hash_symbolization_try.rb
|
2
|
+
|
3
|
+
# NOTE: These testcases are disabled b/c there's a shared context
|
4
|
+
# bug in Tryouts 3.1 that prevents the setup instance vars from
|
5
|
+
# being available to the testcases.
|
6
|
+
|
7
|
+
require_relative '../../lib/familia'
|
8
|
+
require_relative '../helpers/test_helpers'
|
3
9
|
|
4
10
|
Familia.debug = false
|
5
11
|
|
6
12
|
# Test the updated deserialize_value method
|
7
13
|
class SymbolizeTest < Familia::Horreum
|
8
|
-
|
14
|
+
identifier_field :id
|
9
15
|
field :id
|
10
16
|
field :config
|
11
17
|
end
|
12
18
|
|
19
|
+
@test_hash = { "name" => "John", "age" => 30, "nested" => { "theme" => "dark" } }
|
13
20
|
@test_obj = SymbolizeTest.new
|
14
21
|
@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
22
|
@test_obj.config = @test_hash
|
19
23
|
@test_obj.save
|
20
24
|
|
@@ -85,6 +89,7 @@ end
|
|
85
89
|
@test_obj.deserialize_value('just a string')
|
86
90
|
#=> "just a string"
|
87
91
|
|
92
|
+
## A stringified number is still a stringified number
|
88
93
|
@test_obj.deserialize_value('42')
|
89
94
|
#=> "42"
|
90
95
|
|
@@ -92,5 +97,5 @@ end
|
|
92
97
|
@test_obj.deserialize_value('invalid json')
|
93
98
|
#=> "invalid json"
|
94
99
|
|
95
|
-
|
100
|
+
# Clean up
|
96
101
|
@test_obj.destroy!
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# try/edge_cases/json_serialization_try.rb
|
2
|
+
|
3
|
+
require_relative '../../lib/familia'
|
4
|
+
require_relative '../helpers/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_field :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
|
+
|
18
|
+
## Test 1: Store a Hash - should serialize to JSON automatically
|
19
|
+
test_obj = JsonTest.new
|
20
|
+
test_obj.config = { theme: "dark", notifications: true, settings: { volume: 80 } }
|
21
|
+
test_obj.config
|
22
|
+
#=:> Hash
|
23
|
+
|
24
|
+
## Test 2: Store an Array - should serialize to JSON automatically
|
25
|
+
test_obj = JsonTest.new
|
26
|
+
test_obj.tags = ["ruby", "valkey", "json", "familia"]
|
27
|
+
test_obj.tags
|
28
|
+
#=:> Array
|
29
|
+
|
30
|
+
## Test 3: Store a simple string - should remain as string
|
31
|
+
test_obj = JsonTest.new
|
32
|
+
test_obj.simple = "just a string"
|
33
|
+
test_obj.simple
|
34
|
+
#=:> String
|
35
|
+
|
36
|
+
## Save the object - this should call serialize_value and use to_json
|
37
|
+
test_obj = JsonTest.new 'a_unque_id'
|
38
|
+
test_obj.save
|
39
|
+
#=> true
|
40
|
+
|
41
|
+
## Verify what's actually stored in Database (raw)
|
42
|
+
test_obj = JsonTest.new
|
43
|
+
test_obj.id = "json_test_1"
|
44
|
+
test_obj.config = { theme: "dark", notifications: true, settings: { volume: 80 } }
|
45
|
+
test_obj.simple = "just a string"
|
46
|
+
test_obj.tags = ["ruby", "valkey", "json", "familia"]
|
47
|
+
test_obj.save
|
48
|
+
test_obj.hgetall
|
49
|
+
#=> {"id"=>"json_test_1", "config"=>"{\"theme\":\"dark\",\"notifications\":true,\"settings\":{\"volume\":80}}", "tags"=>"[\"ruby\",\"valkey\",\"json\",\"familia\"]", "simple"=>"just a string"}
|
50
|
+
|
51
|
+
## Test 4: Hash should be deserialized back to Hash
|
52
|
+
test_obj = JsonTest.new 'any_id_will_do'
|
53
|
+
puts "Config after refresh:"
|
54
|
+
puts test_obj.config
|
55
|
+
puts "Config class: "
|
56
|
+
[test_obj.config.class, test_obj.config]
|
57
|
+
##=> [Hash, {:theme=>"dark", :notifications=>true, :settings=>{:volume=>80}}]
|
58
|
+
|
59
|
+
## Test 5: Array should be deserialized back to Array
|
60
|
+
test_obj = JsonTest.new 'any_id_will_do'
|
61
|
+
puts "Tags after refresh:"
|
62
|
+
puts test_obj.tags.inspect
|
63
|
+
puts "Tags class: #{test_obj.tags.class}"
|
64
|
+
test_obj.tags.inspect
|
65
|
+
test_obj.tags
|
66
|
+
##=> ["ruby", "valkey", "json", "familia"]
|
67
|
+
|
68
|
+
## Test 6: Simple string should remain a string (this works correctly)
|
69
|
+
test_obj = JsonTest.new 'any_id_will_do'
|
70
|
+
puts "Simple after refresh:"
|
71
|
+
puts test_obj.simple.inspect
|
72
|
+
puts "Simple class: #{test_obj.simple.class}"
|
73
|
+
[test_obj.simple.class, test_obj.simple]
|
74
|
+
##=> [String, "just a string"]
|
75
|
+
|
76
|
+
# Demonstrate the asymmetry:
|
77
|
+
test_obj = JsonTest.new 'any_id_will_do'
|
78
|
+
puts "\n=== ASYMMETRY DEMONSTRATION ==="
|
79
|
+
puts "Before save: config is #{test_obj.config.class}"
|
80
|
+
test_obj.config = { example: "data" }
|
81
|
+
puts "After assignment: config is #{test_obj.config.class}"
|
82
|
+
test_obj.save
|
83
|
+
puts "After save: config is still #{test_obj.config.class}"
|
84
|
+
test_obj.refresh!
|
85
|
+
puts "After refresh: config is now #{test_obj.config.class}!"
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
# Test connection race conditions
|
4
|
+
group "Race Conditions Edge Cases"
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@user_class = Class.new(Familia::Horreum) do
|
8
|
+
identifier :email
|
9
|
+
field :counter
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
try "concurrent connection access causes race condition" do
|
14
|
+
user = @user_class.new(email: "test@example.com", counter: 0)
|
15
|
+
user.save
|
16
|
+
|
17
|
+
threads = []
|
18
|
+
results = []
|
19
|
+
|
20
|
+
# Simulate high concurrency
|
21
|
+
10.times do
|
22
|
+
threads << Thread.new do
|
23
|
+
begin
|
24
|
+
user.incr(:counter)
|
25
|
+
results << "success"
|
26
|
+
rescue => e
|
27
|
+
results << "error: #{e.class.name}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
threads.each(&:join)
|
33
|
+
|
34
|
+
# May show race condition issues
|
35
|
+
errors = results.count { |r| r.start_with?("error") }
|
36
|
+
errors > 0 # Expects some race condition errors
|
37
|
+
ensure
|
38
|
+
user&.delete!
|
39
|
+
end
|
40
|
+
|
41
|
+
try "connection pool stress test" do
|
42
|
+
users = []
|
43
|
+
|
44
|
+
# Create multiple users concurrently
|
45
|
+
threads = []
|
46
|
+
20.times do |i|
|
47
|
+
threads << Thread.new do
|
48
|
+
user = @user_class.new(email: "user#{i}@example.com")
|
49
|
+
user.save
|
50
|
+
users << user
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
threads.each(&:join)
|
55
|
+
|
56
|
+
# Check for connection issues
|
57
|
+
users.length > 0 # Some should succeed despite race conditions
|
58
|
+
ensure
|
59
|
+
users.each(&:delete!) rescue nil
|
60
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
# Test reserved keyword handling
|
4
|
+
group "Reserved Keywords Edge Cases"
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@user_class = Class.new(Familia::Horreum) do
|
8
|
+
identifier :email
|
9
|
+
# These should fail with reserved keywords
|
10
|
+
begin
|
11
|
+
field :ttl # Reserved for expiration
|
12
|
+
field :db # Reserved for database
|
13
|
+
field :redis # Reserved for connection
|
14
|
+
rescue => e
|
15
|
+
# Expected to fail
|
16
|
+
end
|
17
|
+
|
18
|
+
# Workarounds
|
19
|
+
field :secret_ttl
|
20
|
+
field :user_db
|
21
|
+
field :redis_config
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
try "cannot use ttl as field name" do
|
26
|
+
begin
|
27
|
+
Class.new(Familia::Horreum) do
|
28
|
+
field :ttl
|
29
|
+
end
|
30
|
+
false # Should not reach here
|
31
|
+
rescue => e
|
32
|
+
true # Expected error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
try "workaround with prefixed names works" do
|
37
|
+
user = @user_class.new(email: "test@example.com")
|
38
|
+
user.secret_ttl = 3600
|
39
|
+
user.user_db = 5
|
40
|
+
user.redis_config = {host: "localhost"}
|
41
|
+
user.save
|
42
|
+
|
43
|
+
user.secret_ttl == 3600 &&
|
44
|
+
user.user_db == 5 &&
|
45
|
+
user.redis_config.is_a?(Hash)
|
46
|
+
ensure
|
47
|
+
user&.delete!
|
48
|
+
end
|
49
|
+
|
50
|
+
try "reserved methods still work normally" do
|
51
|
+
user = @user_class.new(email: "test@example.com")
|
52
|
+
user.save
|
53
|
+
|
54
|
+
user.respond_to?(:ttl) &&
|
55
|
+
user.respond_to?(:db) &&
|
56
|
+
user.respond_to?(:redis)
|
57
|
+
ensure
|
58
|
+
user&.delete!
|
59
|
+
end
|
@@ -1,35 +1,63 @@
|
|
1
|
-
# try/
|
1
|
+
# try/edge_cases/string_coercion_try.rb
|
2
2
|
|
3
|
-
require_relative '../
|
4
|
-
require_relative './test_helpers'
|
3
|
+
require_relative '../helpers/test_helpers'
|
5
4
|
|
6
5
|
Familia.debug = false
|
7
6
|
|
7
|
+
@customer_id = 'customer-string-coercion-test'
|
8
|
+
|
8
9
|
# Error handling: object without proper identifier setup
|
9
10
|
class ::BadIdentifierTest < Familia::Horreum
|
10
11
|
# No identifier method defined - should cause issues
|
11
12
|
end
|
12
13
|
|
14
|
+
# Case statement works with string matching
|
15
|
+
def classify_id(obj)
|
16
|
+
case obj.to_s
|
17
|
+
when /customer/
|
18
|
+
'customer_type'
|
19
|
+
when /session/
|
20
|
+
'session_type'
|
21
|
+
else
|
22
|
+
'unknown_type'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Polymorphic method accepting both strings and Familia objects
|
27
|
+
def process_identifier(id_or_object)
|
28
|
+
# Can handle both string IDs and Familia objects uniformly
|
29
|
+
processed_id = id_or_object.to_s
|
30
|
+
"processed:#{processed_id}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def lookup_by_id(id_string)
|
34
|
+
id_string.to_s.upcase
|
35
|
+
end
|
36
|
+
|
37
|
+
## Instantiaite a troubled model class
|
13
38
|
@bad_obj = ::BadIdentifierTest.new
|
39
|
+
#=:> BadIdentifierTest
|
14
40
|
|
15
41
|
# Test polymorphic string usage for Familia objects
|
16
|
-
@customer_id = 'customer-string-coercion-test'
|
17
42
|
@customer = Customer.new(@customer_id)
|
18
43
|
@customer.name = 'John Doe'
|
19
44
|
@customer.planid = 'premium'
|
20
45
|
@customer.save
|
46
|
+
#=> true
|
21
47
|
|
22
|
-
|
23
|
-
@session = Session.new
|
48
|
+
## Session save
|
49
|
+
@session = Session.new
|
24
50
|
@session.custid = @customer_id
|
25
51
|
@session.useragent = 'Test Browser'
|
26
52
|
@session.save
|
53
|
+
#=> true
|
27
54
|
|
28
|
-
##
|
55
|
+
## Bone with simple identifier
|
29
56
|
@bone = Bone.new
|
30
57
|
@bone.token = 'test_token'
|
31
58
|
@bone.name = 'test_name'
|
32
|
-
|
59
|
+
@bone.identifier
|
60
|
+
#=> 'test_token'
|
33
61
|
|
34
62
|
## Basic to_s functionality returns identifier
|
35
63
|
@customer.to_s
|
@@ -43,14 +71,20 @@ end
|
|
43
71
|
@customer.to_s == @customer.identifier
|
44
72
|
#=> true
|
45
73
|
|
74
|
+
## Session identifier can be set and is not overidden even after saved
|
75
|
+
@session_id = 'session-string-coercion-test'
|
76
|
+
session = Session.new(@session_id)
|
77
|
+
session.identifier
|
78
|
+
session
|
79
|
+
#==> _.to_s == @session_id
|
80
|
+
#==> _.identifier == @session_id
|
81
|
+
#==> _.save
|
82
|
+
#==> _.identifier == @session_id
|
83
|
+
#==> _.to_s == @session_id
|
84
|
+
|
46
85
|
## Session to_s works with generated identifier
|
47
86
|
@session.to_s
|
48
|
-
|
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
|
87
|
+
#=<> @session_id
|
54
88
|
|
55
89
|
lookup_by_id(@customer)
|
56
90
|
#=> @customer_id.upcase
|
@@ -64,7 +98,7 @@ lookup_by_id(@customer)
|
|
64
98
|
## Array operations work with mixed types
|
65
99
|
@mixed_array = [@customer_id, @customer, @session]
|
66
100
|
@mixed_array.map(&:to_s)
|
67
|
-
#=> [@customer_id, @customer_id,
|
101
|
+
#=> [@customer_id, @customer_id, _[2]]
|
68
102
|
|
69
103
|
## String comparison works
|
70
104
|
@customer.to_s == @customer_id
|
@@ -72,75 +106,42 @@ lookup_by_id(@customer)
|
|
72
106
|
|
73
107
|
## Join operations work seamlessly
|
74
108
|
[@customer, 'separator', @session].join(':')
|
75
|
-
|
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
|
109
|
+
#=~> /\A#{@customer_id}:separator:[0-9a-z]+\z/
|
88
110
|
|
111
|
+
## Classify a customer
|
89
112
|
classify_id(@customer)
|
90
113
|
#=> 'customer_type'
|
91
114
|
|
92
|
-
|
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
|
-
|
115
|
+
## Polymorphic method accepting both strings and Familia objects (string)
|
102
116
|
process_identifier(@customer_id)
|
103
117
|
#=> "processed:#{@customer_id}"
|
104
118
|
|
119
|
+
## Polymorphic method accepting both strings and Familia objects (familia)
|
105
120
|
process_identifier(@customer)
|
106
121
|
#=> "processed:#{@customer_id}"
|
107
122
|
|
108
|
-
##
|
123
|
+
## Database storage using object as string key
|
109
124
|
@metadata = Familia::HashKey.new 'metadata'
|
110
125
|
@metadata[@customer] = 'customer_metadata'
|
111
126
|
@metadata[@customer.to_s] # Same key access
|
112
127
|
#=> 'customer_metadata'
|
113
128
|
|
114
|
-
## Cleanup after test
|
129
|
+
## Cleanup after test, 1
|
115
130
|
@metadata.delete!
|
116
131
|
#=> true
|
117
132
|
|
133
|
+
## Cleanup after test, 2
|
118
134
|
@customer.delete!
|
119
135
|
#=> true
|
120
136
|
|
137
|
+
## Cleanup after test, 3
|
121
138
|
@session.delete!
|
122
139
|
#=> true
|
123
140
|
|
124
141
|
## to_s handles identifier errors gracefully
|
125
|
-
|
126
|
-
|
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
|
142
|
+
badboi = BadIdentifierTest.new
|
143
|
+
badboi.to_s #.include?('BadIdentifierTest')
|
144
|
+
#=~> /BadIdentifierTest:0x[0-9a-f]+/
|
144
145
|
|
145
146
|
## Performance consideration: to_s caching behavior
|
146
147
|
@customer2 = Customer.new('performance-test')
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative '../helpers/test_helpers'
|
2
|
+
|
3
|
+
# Test TTL side effects
|
4
|
+
group "TTL Side Effects Edge Cases"
|
5
|
+
|
6
|
+
setup do
|
7
|
+
@session_class = Class.new(Familia::Horreum) do
|
8
|
+
identifier :session_id
|
9
|
+
field :name
|
10
|
+
field :data
|
11
|
+
feature :expiration
|
12
|
+
ttl 300 # 5 minutes
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
try "field update unintentionally resets TTL" do
|
17
|
+
session = @session_class.new(session_id: "test123", name: "Session")
|
18
|
+
session.save
|
19
|
+
|
20
|
+
# Set shorter TTL
|
21
|
+
session.expire(60)
|
22
|
+
original_ttl = session.realttl
|
23
|
+
|
24
|
+
# Update field - this may reset TTL unexpectedly
|
25
|
+
session.name = "Updated Session"
|
26
|
+
session.save
|
27
|
+
|
28
|
+
new_ttl = session.realttl
|
29
|
+
|
30
|
+
# TTL should remain short but may have been reset
|
31
|
+
new_ttl > original_ttl # Indicates TTL side effect
|
32
|
+
ensure
|
33
|
+
session&.delete!
|
34
|
+
end
|
35
|
+
|
36
|
+
try "batch update preserves TTL with flag" do
|
37
|
+
session = @session_class.new(session_id: "test124")
|
38
|
+
session.save
|
39
|
+
session.expire(60)
|
40
|
+
|
41
|
+
original_ttl = session.realttl
|
42
|
+
|
43
|
+
# Use update_expiration: false to preserve TTL
|
44
|
+
session.batch_update({name: "Batch Updated"}, update_expiration: false)
|
45
|
+
|
46
|
+
new_ttl = session.realttl
|
47
|
+
|
48
|
+
(original_ttl - new_ttl).abs < 5 # TTL preserved within tolerance
|
49
|
+
ensure
|
50
|
+
session&.delete!
|
51
|
+
end
|