familia 1.2.3 → 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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +68 -0
  3. data/.github/workflows/docs.yml +64 -0
  4. data/.gitignore +3 -0
  5. data/.pre-commit-config.yaml +3 -1
  6. data/.rubocop.yml +16 -9
  7. data/.rubocop_todo.yml +177 -31
  8. data/.yardopts +9 -0
  9. data/CLAUDE.md +141 -0
  10. data/Gemfile +15 -2
  11. data/Gemfile.lock +61 -61
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +317 -0
  15. data/familia.gemspec +8 -5
  16. data/lib/familia/base.rb +19 -9
  17. data/lib/familia/connection.rb +232 -65
  18. data/lib/familia/core_ext.rb +1 -1
  19. data/lib/familia/datatype/commands.rb +59 -0
  20. data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
  21. data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
  22. data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
  23. data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
  24. data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
  25. data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
  26. data/lib/familia/datatype.rb +243 -0
  27. data/lib/familia/errors.rb +5 -2
  28. data/lib/familia/features/expiration.rb +33 -34
  29. data/lib/familia/features/quantization.rb +9 -3
  30. data/lib/familia/features/safe_dump.rb +2 -3
  31. data/lib/familia/features.rb +2 -2
  32. data/lib/familia/horreum/class_methods.rb +97 -130
  33. data/lib/familia/horreum/commands.rb +46 -51
  34. data/lib/familia/horreum/connection.rb +82 -0
  35. data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
  36. data/lib/familia/horreum/serialization.rb +61 -198
  37. data/lib/familia/horreum/settings.rb +6 -17
  38. data/lib/familia/horreum/utils.rb +11 -10
  39. data/lib/familia/horreum.rb +69 -60
  40. data/lib/familia/logging.rb +12 -12
  41. data/lib/familia/multi_result.rb +72 -0
  42. data/lib/familia/refinements.rb +7 -44
  43. data/lib/familia/settings.rb +11 -11
  44. data/lib/familia/utils.rb +123 -90
  45. data/lib/familia/version.rb +4 -21
  46. data/lib/familia.rb +17 -12
  47. data/lib/middleware/database_middleware.rb +150 -0
  48. data/try/configuration/scenarios_try.rb +65 -0
  49. data/try/core/connection_try.rb +58 -0
  50. data/try/core/errors_try.rb +93 -0
  51. data/try/core/extensions_try.rb +26 -0
  52. data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
  53. data/try/{00_familia_try.rb → core/familia_try.rb} +5 -3
  54. data/try/core/middleware_try.rb +68 -0
  55. data/try/core/refinements_try.rb +39 -0
  56. data/try/core/settings_try.rb +76 -0
  57. data/try/core/tools_try.rb +54 -0
  58. data/try/core/utils_try.rb +189 -0
  59. data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
  60. data/try/datatypes/datatype_base_try.rb +69 -0
  61. data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
  62. data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
  63. data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
  64. data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
  65. data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
  66. data/try/edge_cases/empty_identifiers_try.rb +48 -0
  67. data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -8
  68. data/try/edge_cases/json_serialization_try.rb +85 -0
  69. data/try/edge_cases/race_conditions_try.rb +60 -0
  70. data/try/edge_cases/reserved_keywords_try.rb +59 -0
  71. data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +63 -60
  72. data/try/edge_cases/ttl_side_effects_try.rb +51 -0
  73. data/try/features/expiration_try.rb +86 -0
  74. data/try/features/quantization_try.rb +90 -0
  75. data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
  76. data/try/features/safe_dump_try.rb +137 -0
  77. data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
  78. data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
  79. data/try/horreum/class_methods_try.rb +41 -0
  80. data/try/horreum/commands_try.rb +49 -0
  81. data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
  82. data/try/horreum/relations_try.rb +146 -0
  83. data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
  84. data/try/horreum/settings_try.rb +43 -0
  85. data/try/integration/cross_component_try.rb +46 -0
  86. data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
  87. data/try/{40_customer_try.rb → models/customer_try.rb} +20 -17
  88. data/try/models/datatype_base_try.rb +101 -0
  89. data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
  90. data/try/performance/benchmarks_try.rb +55 -0
  91. data/try/pooling/README.md +20 -0
  92. data/try/pooling/configurable_stress_test_try.rb +435 -0
  93. data/try/pooling/connection_pool_test_try.rb +273 -0
  94. data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  95. data/try/pooling/lib/connection_pool_metrics.rb +372 -0
  96. data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
  97. data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
  98. data/try/pooling/lib/visualize_stress_results.rb +434 -0
  99. data/try/pooling/pool_siege_try.rb +509 -0
  100. data/try/pooling/run_stress_tests_try.rb +482 -0
  101. data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
  102. data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
  103. data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
  104. data/try/prototypes/atomic_saves_v4.rb +105 -0
  105. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
  106. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  107. metadata +124 -38
  108. data/.github/workflows/ruby.yml +0 -71
  109. data/VERSION.yml +0 -4
  110. data/lib/familia/redistype/commands.rb +0 -59
  111. data/lib/familia/redistype.rb +0 -228
  112. data/lib/familia/tools.rb +0 -68
  113. data/lib/redis_middleware.rb +0 -109
  114. data/try/20_redis_type_try.rb +0 -70
  115. data/try/91_json_bug_try.rb +0 -86
@@ -0,0 +1,86 @@
1
+ # try/features/expiration_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ # Test expiration feature functionality
9
+
10
+ class ParentExpiring < Familia::Horreum
11
+ feature :expiration
12
+ default_expiration 1000
13
+ end
14
+
15
+ class ChildExpiring < ParentExpiring
16
+ identifier_field :id
17
+ field :id
18
+ default_expiration 1000
19
+ end
20
+
21
+ # Define a test class with expiration feature
22
+ class ExpiringTest < Familia::Horreum
23
+ feature :expiration
24
+ identifier_field :id
25
+ field :id
26
+ field :data
27
+ default_expiration 300 # 5 minutes
28
+ end
29
+
30
+ # Setup test object
31
+ @test_obj = ExpiringTest.new
32
+ @test_obj.id = "expire_test_1"
33
+ @test_obj.data = "test data"
34
+
35
+ ## Class has default_expiration method from feature
36
+ ExpiringTest.respond_to?(:default_expiration)
37
+ #=> true
38
+
39
+ ## Class has default expiration set
40
+ ExpiringTest.default_expiration
41
+ #=> 300.0
42
+
43
+ ## Can set different default expiration on class
44
+ ExpiringTest.default_expiration(600)
45
+ ExpiringTest.default_expiration
46
+ #=> 600.0
47
+
48
+ ## Object inherits class default expiration
49
+ @test_obj.default_expiration
50
+ #=> 600.0
51
+
52
+ ## Can set default expiration on individual object
53
+ @test_obj.default_expiration = 120
54
+ @test_obj.default_expiration
55
+ #=> 120.0
56
+
57
+ ## Object has update_expiration method
58
+ @test_obj.respond_to?(:update_expiration)
59
+ #=> true
60
+
61
+ ## Can call update_expiration method
62
+ result = @test_obj.update_expiration(default_expiration: 180)
63
+ [result.class, result]
64
+ #=> [FalseClass, false]
65
+
66
+ ## Child inherits parent default expiration when not set
67
+ ChildExpiring.default_expiration
68
+ #=> 1000.0
69
+
70
+ ## Can override parent default expiration
71
+ ChildExpiring.default_expiration(500)
72
+ ChildExpiring.default_expiration
73
+ #=> 500.0
74
+
75
+ ## Falls back to Familia.default_expiration when no class/parent default expiration
76
+ class NoDefaultExpirationTest < Familia::Horreum
77
+ feature :expiration
78
+ identifier_field :id
79
+ field :id
80
+ end
81
+ NoDefaultExpirationTest.default_expiration
82
+ #=> 0.0
83
+
84
+ # Cleanup
85
+ @test_obj.destroy!
86
+ ExpiringTest.default_expiration(300) # Reset to original
@@ -0,0 +1,90 @@
1
+ # try/features/quantization_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ # Test quantization feature functionality
9
+
10
+ # Define a test class with quantization feature
11
+ class QuantizedTest < Familia::Horreum
12
+ feature :quantization
13
+ identifier_field :id
14
+ field :id
15
+ field :data
16
+ default_expiration 300 # 5 minutes for testing
17
+ end
18
+
19
+ ## Class has qstamp method from feature
20
+ QuantizedTest.respond_to?(:qstamp)
21
+ #=> true
22
+
23
+ ## Object has qstamp method from feature
24
+ @test_obj = QuantizedTest.new
25
+ @test_obj.respond_to?(:qstamp)
26
+ #=> true
27
+
28
+ ## Familia has global qstamp method
29
+ Familia.respond_to?(:qstamp)
30
+ #=> true
31
+
32
+ ## Familia has now method for current time
33
+ Familia.respond_to?(:now)
34
+ #=> true
35
+
36
+ ## Can get current time with Familia.now
37
+ now = Familia.now
38
+ now.class
39
+ #=> Float
40
+
41
+ ## qstamp with no arguments returns current quantized timestamp
42
+ stamp1 = QuantizedTest.qstamp
43
+ p [:QuantizedTest, QuantizedTest.qstamp]
44
+ stamp1.class
45
+ #=> Integer
46
+
47
+ ## qstamp with quantum returns quantized timestamp
48
+ stamp2 = QuantizedTest.qstamp(3600) # 1 hour quantum
49
+ stamp2.class
50
+ #=> Integer
51
+
52
+ ## qstamp with quantum and pattern returns formatted string
53
+ stamp3 = QuantizedTest.qstamp(3600, pattern: '%Y%m%d%H')
54
+ stamp3.class
55
+ #=> String
56
+
57
+ ## qstamp with time argument
58
+ stamp4 = QuantizedTest.qstamp(3600, pattern: '%Y%m%d%H', time: Time.new(2023, 6, 15, 14, 30, 0))
59
+ stamp4.class
60
+ #=> String
61
+
62
+ ## Object qstamp works same as class method
63
+ obj_stamp = @test_obj.qstamp(3600)
64
+ obj_stamp.class
65
+ #=> Integer
66
+
67
+ ## Different quantum values produce different buckets
68
+ test_time = Time.utc(2023, 6, 15, 14, 30, 0) # use a fixed time, mid-day (avoid ToD boundary)
69
+ @hour_stamp = QuantizedTest.qstamp(3600, time: test_time)
70
+ @day_stamp = QuantizedTest.qstamp(86400, time: test_time)
71
+ #=> 1686787200
72
+ #=<> @hour_stamp
73
+ #==> @hour_stamp == 1686837600
74
+
75
+ ## Pattern formatting works correctly
76
+ time_str = QuantizedTest.qstamp(3600, pattern: '%Y-%m-%d %H:00:00')
77
+ time_str.match?(/\d{4}-\d{2}-\d{2} \d{2}:00:00/)
78
+ #=> true
79
+
80
+ ## Can pass custom time to qstamp
81
+ test_time = Time.utc(2023, 6, 15, 14, 30, 1)
82
+ # NOTE: _Not_ Time.new(2023, 6, 15, 14, 30, 1).utc which is the current time
83
+ # locally where this code is running, then converted to UTC.
84
+ custom_stamp = QuantizedTest.qstamp(3600, pattern: '%Y%m%d%H', time: test_time)
85
+ custom_stamp
86
+ #=> "2023061514"
87
+
88
+ # Cleanup
89
+ @test_obj.id = "quantized_test_obj" # Set identifier before cleanup
90
+ @test_obj.destroy! if @test_obj
@@ -1,9 +1,10 @@
1
- # frozen_string_literal: true
1
+ # try/features/safe_dump_extended_try.rb
2
+
2
3
 
3
4
  # These tryouts test the safe dumping functionality.
4
5
 
5
- require_relative '../lib/familia'
6
- require_relative './test_helpers'
6
+ require_relative '../../lib/familia'
7
+ require_relative '../helpers/test_helpers'
7
8
 
8
9
  ## By default Familia::Base has no safe_dump_fields method
9
10
  Familia::Base.respond_to?(:safe_dump_fields)
@@ -42,12 +43,12 @@ Customer.safe_dump_fields
42
43
  @cust2.email = "test@example.com"
43
44
  @cust2.custid = "test@example.com"
44
45
  @all_safe_fields = @cust2.safe_dump.keys.sort
45
-
46
46
  @all_non_safe_fields = @cust2.instance_variables.map { |el|
47
47
  el.to_s[1..-1].to_sym # slice off the leading @
48
48
  }.sort
49
+ # Check if any of the non-safe fields are in the safe dump (tryouts bug
50
+ # if this comment is placed right before the last line.)
49
51
  p [1, all_non_safe_fields: @all_non_safe_fields]
50
- # Check if any of the non-safe fields are in the safe dump
51
52
  (@all_non_safe_fields & @all_safe_fields) - [:custid, :role, :verified, :updated, :created, :secrets_created]
52
53
  #=> []
53
54
 
@@ -56,7 +57,7 @@ Bone.respond_to?(:safe_dump_fields)
56
57
  #=> false
57
58
 
58
59
  ## Bone instances do not have safe_dump method
59
- @bone = Bone.new(name: "Rex", age: 3)
60
+ @bone = Bone.new(token: "boneid1", name: "Rex")
60
61
  @bone.respond_to?(:safe_dump)
61
62
  #=> false
62
63
 
@@ -0,0 +1,137 @@
1
+ # try/features/safe_dump_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ # Test SafeDump feature functionality
9
+
10
+ # Define a test class with SafeDump feature
11
+ class SafeDumpTest < Familia::Horreum
12
+ feature :safe_dump
13
+ identifier_field :id
14
+ field :id
15
+ field :name
16
+ field :email
17
+ field :secret_data
18
+
19
+ @safe_dump_fields = [
20
+ :id,
21
+ :name,
22
+ { :display_name => ->(obj) { "#{obj.name} (#{obj.id})" } },
23
+ { :has_email => ->(obj) { !obj.email.nil? && !obj.email.empty? } }
24
+ ]
25
+
26
+ def active?
27
+ true
28
+ end
29
+ end
30
+
31
+ # Setup test object
32
+ @test_obj = SafeDumpTest.new
33
+ @test_obj.id = "safe_test_1"
34
+ @test_obj.name = "Test User"
35
+ @test_obj.email = "test@example.com"
36
+ @test_obj.secret_data = "sensitive_info"
37
+
38
+ ## Class has SafeDump methods
39
+ SafeDumpTest.respond_to?(:safe_dump_fields)
40
+ #=> true
41
+
42
+ ## Class has safe_dump_field_map method
43
+ SafeDumpTest.respond_to?(:safe_dump_field_map)
44
+ #=> true
45
+
46
+ ## Object has safe_dump method
47
+ @test_obj.respond_to?(:safe_dump)
48
+ #=> true
49
+
50
+ ## safe_dump_fields returns field names only
51
+ fields = SafeDumpTest.safe_dump_fields
52
+ fields
53
+ #=> [:id, :name, :display_name, :has_email]
54
+
55
+ ## safe_dump_field_map returns callable map
56
+ field_map = SafeDumpTest.safe_dump_field_map
57
+ field_map.keys.sort
58
+ #=> [:display_name, :has_email, :id, :name]
59
+
60
+ ## safe_dump_field_map values are callable
61
+ field_map = SafeDumpTest.safe_dump_field_map
62
+ field_map[:id]
63
+ #==> _.respond_to?(:call)
64
+
65
+ ## safe_dump returns hash with safe fields only
66
+ dump = @test_obj.safe_dump
67
+ dump.keys.sort
68
+ #=> [:display_name, :has_email, :id, :name]
69
+
70
+ ## safe_dump includes basic field values
71
+ dump = @test_obj.safe_dump
72
+ [dump[:id], dump[:name]]
73
+ #=> ["safe_test_1", "Test User"]
74
+
75
+ ## safe_dump includes computed field values
76
+ dump = @test_obj.safe_dump
77
+ dump[:display_name]
78
+ #=> "Test User (safe_test_1)"
79
+
80
+ ## safe_dump includes lambda field values
81
+ dump = @test_obj.safe_dump
82
+ dump[:has_email]
83
+ #=> true
84
+
85
+ ## safe_dump excludes non-whitelisted fields
86
+ dump = @test_obj.safe_dump
87
+ dump.key?(:secret_data)
88
+ #=> false
89
+
90
+ ## safe_dump excludes email field not in whitelist
91
+ dump = @test_obj.safe_dump
92
+ dump.key?(:email)
93
+ #=> false
94
+
95
+ ## Safe dump works with nil values
96
+ @test_obj.email = nil
97
+ dump = @test_obj.safe_dump
98
+ dump[:has_email]
99
+ #=> false
100
+
101
+ ## Safe dump works with empty values
102
+ @test_obj.email = ""
103
+ dump = @test_obj.safe_dump
104
+ dump[:has_email]
105
+ #=> false
106
+
107
+ ## Can define safe_dump_fields with set_safe_dump_fields
108
+ class DynamicSafeDump < Familia::Horreum
109
+ feature :safe_dump
110
+ identifier_field :id
111
+ field :id
112
+ field :data
113
+ end
114
+
115
+ DynamicSafeDump.set_safe_dump_fields(:id, :data)
116
+ DynamicSafeDump.safe_dump_fields
117
+ #=> [:id, :data]
118
+
119
+ ## Class with no safe_dump_fields defined has empty array
120
+ class EmptySafeDump < Familia::Horreum
121
+ feature :safe_dump
122
+ identifier_field :id
123
+ field :id
124
+ end
125
+
126
+ EmptySafeDump.safe_dump_fields
127
+ #=> []
128
+
129
+ ## Empty safe_dump returns empty hash
130
+ @empty_obj = EmptySafeDump.new
131
+ @empty_obj.id = "empty_test"
132
+ @empty_obj.safe_dump
133
+ #=> {}
134
+
135
+ # Cleanup
136
+ @test_obj.destroy! if @test_obj
137
+ @empty_obj.destroy! if @empty_obj
@@ -1,14 +1,17 @@
1
- # rubocop:disable all
1
+ # try/helpers/test_helpers.rb
2
+
3
+ # To enable tracing and debug mode, run with the env vars set.
4
+ #
5
+ # e.g. FAMILIA_TRACE=1 FAMILIA_DEBUG=1 bundle exec try
2
6
 
3
7
  require 'digest'
4
- require_relative '../lib/familia'
8
+ require_relative '../../lib/familia'
5
9
 
6
- Familia.debug = false # also # ENV['FAMILIA_TRACE'] = '1'
7
- Familia.enable_redis_logging = true
8
- Familia.enable_redis_counter = true
10
+ Familia.enable_database_logging = true
11
+ Familia.enable_database_counter = true
9
12
 
10
13
  class Bone < Familia::Horreum
11
- identifier [:token, :name]
14
+ identifier_field :token
12
15
  field :token
13
16
  field :name
14
17
  list :owners
@@ -28,8 +31,8 @@ class Blone < Familia::Horreum
28
31
  end
29
32
 
30
33
  class Customer < Familia::Horreum
31
- db 15 # don't use Onetime's default DB
32
- ttl 5.years
34
+ logical_database 15 # Use something other than the default DB
35
+ default_expiration 5.years
33
36
 
34
37
  feature :safe_dump
35
38
  #feature :expiration
@@ -63,13 +66,12 @@ class Customer < Familia::Horreum
63
66
 
64
67
  counter :secrets_created
65
68
 
66
- identifier :custid
69
+ identifier_field :custid
67
70
 
68
71
  field :custid
69
72
  field :sessid
70
73
  field :email
71
74
  field :role
72
- field :key
73
75
  field :name
74
76
  field :passphrase_encryption
75
77
  field :passphrase
@@ -96,49 +98,24 @@ end
96
98
  @c.custid = "d@example.com"
97
99
 
98
100
  class Session < Familia::Horreum
99
- db 14 # don't use Onetime's default DB
100
- ttl 180.minutes
101
+ logical_database 14 # don't use Onetime's default DB
102
+ default_expiration 180.minutes
101
103
 
102
- identifier :generate_id
104
+ identifier_field :sessid
103
105
 
104
106
  field :sessid
105
107
  field :shrimp
106
108
  field :custid
107
109
  field :useragent
108
- field :key
109
110
  field :authenticated
110
111
  field :ipaddress
111
112
  field :created
112
113
  field :updated
113
114
 
114
- def generate_id
115
- @sessid ||= Familia.generate_id
116
- @sessid
117
- end
118
-
119
- # The external identifier is used by the rate limiter to estimate a unique
120
- # client. We can't use the session ID b/c the request agent can choose to
121
- # not send cookies, or the user can clear their cookies (in both cases the
122
- # session ID would change which would circumvent the rate limiter). The
123
- # external identifier is a hash of the IP address and the customer ID
124
- # which means that anonymous users from the same IP address are treated
125
- # as the same client (as far as the limiter is concerned). Not ideal.
126
- #
127
- # To put it another way, the risk of colliding external identifiers is
128
- # acceptable for the rate limiter, but not for the session data. Acceptable
129
- # b/c the rate limiter is a temporary measure to prevent abuse, and the
130
- # worse case scenario is that a user is rate limited when they shouldn't be.
131
- # The session data is permanent and must be kept separate to avoid leaking
132
- # data between users.
133
- def external_identifier
134
- elements = []
135
- elements << ipaddress || 'UNKNOWNIP'
136
- elements << custid || 'anon'
137
- @external_identifier ||= Familia.generate_sha_hash(elements)
138
- Familia.ld "[Session.external_identifier] sess identifier input: #{elements.inspect} (result: #{@external_identifier})"
139
- @external_identifier
115
+ def save
116
+ self.sessid ||= Familia.generate_id # Only generates when persisting
117
+ super
140
118
  end
141
-
142
119
  end
143
120
  @s = Session.new
144
121
 
@@ -146,11 +123,11 @@ class CustomDomain < Familia::Horreum
146
123
 
147
124
  feature :expiration
148
125
 
149
- class_sorted_set :values, key: 'onetime:customdomain:values'
150
-
151
- identifier :derive_id
126
+ class_sorted_set :values
152
127
 
128
+ identifier_field :generate_id
153
129
 
130
+ field :domainid
154
131
  field :display_domain
155
132
  field :custid
156
133
  field :base_domain
@@ -158,7 +135,6 @@ class CustomDomain < Familia::Horreum
158
135
  field :trd
159
136
  field :tld
160
137
  field :sld
161
- # No :key field (so we can test hte behaviour in Horreum#initialize)
162
138
  field :txt_validation_host
163
139
  field :txt_validation_value
164
140
  field :status
@@ -167,18 +143,8 @@ class CustomDomain < Familia::Horreum
167
143
  field :created
168
144
  field :updated
169
145
  field :_original_value
170
-
171
- # Derive a unique identifier for the object based on the display domain and
172
- # the customer ID. This is used to ensure that the same domain can't be
173
- # added twice by the same customer while avoiding collisions between customers.
174
- def derive_id
175
- elements = [
176
- display_domain,
177
- custid
178
- ]
179
- Familia.generate_sha_hash(*elements).slice(0, 8)
180
- end
181
146
  end
147
+
182
148
  @d = CustomDomain.new
183
149
  @d.display_domain = "example.com"
184
150
  @d.custid = @c.custid
@@ -188,12 +154,11 @@ class Limiter < Familia::Horreum
188
154
  feature :expiration
189
155
  feature :quantization
190
156
 
191
- ttl 30.minutes
192
- identifier :name
157
+ identifier_field :name
158
+ default_expiration 30.minutes
193
159
  field :name
194
- # No :key field (so we can test hte behaviour in Horreum#initialize)
195
160
 
196
- string :counter, :ttl => 1.hour, :quantize => [10.minutes, '%H:%M', 1302468980]
161
+ string :counter, :default_expiration => 1.hour, :quantize => [10.minutes, '%H:%M', 1302468980]
197
162
 
198
163
  def identifier
199
164
  @name
@@ -1,5 +1,7 @@
1
- require_relative '../lib/familia'
2
- require_relative './test_helpers'
1
+ # try/horreum/base_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
3
5
 
4
6
  Familia.debug = false
5
7
 
@@ -8,6 +10,9 @@ Familia.debug = false
8
10
  @hashkey = Familia::HashKey.new 'tryouts-27'
9
11
 
10
12
  ## Customer passed as value is returned as string identifier, on assignment as string
13
+ ## TODO: Revisit these @identifier testcases b/c I think we don't want to be setting
14
+ ## the @identifier instance var anymore since identifer_field should only take field
15
+ ## names now (and might be removed altogether).
11
16
  @hashkey["test1"] = @customer.identifier
12
17
  #=> @identifier
13
18
 
@@ -48,7 +53,7 @@ Familia.debug = false
48
53
  #=> true
49
54
 
50
55
  ## Horreum object fields have a fast attribute method (1 of 2)
51
- Familia.trace :LOAD, @customer.redis, @customer.redisuri, caller if Familia.debug?
56
+ Familia.trace :LOAD, @customer.dbclient, @customer.uri, caller if Familia.debug?
52
57
  @customer.name! 'Jane Doe'
53
58
  #=> 0
54
59
 
@@ -68,26 +73,46 @@ Familia.trace :LOAD, @customer.redis, @customer.redisuri, caller if Familia.debu
68
73
  #=> true
69
74
 
70
75
  ## All horrerum objects have a key field
71
- @customer.key
76
+ @customer.identifier
72
77
  #=> @identifier
73
78
 
74
79
  ## Even ones that didn't define it
75
- @cd = CustomDomain.new "www.example.com", "@identifier"
76
- @cd.key
80
+ class NoIdentifierClass < Familia::Horreum
81
+ field :name
82
+ end
83
+ @no_id = NoIdentifierClass.new name: "test"
84
+ @no_id.identifier
77
85
  #=> nil
78
86
 
79
87
  ## We can call #identifier directly if we want to "lasy load" the unique identifier
88
+ @cd = CustomDomain.new display_domain: "www.example.com", custid: "domain-test@example.com"
80
89
  @cd.identifier
81
- #=> "7565befd"
82
-
83
- ## The #key field will still be nil
84
- @cd.key
85
- #=> nil
90
+ #=:> String
91
+ #=/=> _.empty?
92
+ #==> _.size > 16
93
+ #=~>/\A[0-9a-z]+\z/
94
+
95
+ ## The identifier is now memoized (same value each time)
96
+ @cd_first_call = @cd.identifier
97
+ @cd_second_call = @cd.identifier
98
+ @cd_first_call == @cd_second_call
99
+ #=> true
86
100
 
87
101
  ## But once we save
88
102
  @cd.save
89
103
  #=> true
90
104
 
91
- ## The key will be set
92
- @cd.key
93
- #=> "7565befd"
105
+ ## The key has been set now that the instance has been saved
106
+ @cd.identifier
107
+ #=:> String
108
+ #=/=> _.empty?
109
+ #==> _.size > 16
110
+ #=~>/\A[0-9a-z]+\z/
111
+
112
+ ## Array-based identifiers are no longer supported and raise clear errors at class definition time
113
+ class ArrayIdentifierTest < Familia::Horreum
114
+ identifier_field [:token, :name] # This should raise an error immediately
115
+ field :token
116
+ field :name
117
+ end
118
+ #=!> Familia::Problem
@@ -0,0 +1,41 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test Horreum class methods
4
+ group "Horreum Class Methods"
5
+
6
+ setup do
7
+ @user_class = Class.new(Familia::Horreum) do
8
+ identifier :email
9
+ field :name
10
+ field :age
11
+ end
12
+ end
13
+
14
+ try "create factory method with existence checking" do
15
+ user = @user_class.create(email: "test@example.com", name: "Test")
16
+ exists = @user_class.exists?("test@example.com")
17
+
18
+ user.is_a?(@user_class) && exists
19
+ ensure
20
+ @user_class.destroy!("test@example.com")
21
+ end
22
+
23
+ try "multiget retrieves multiple objects" do
24
+ @user_class.create(email: "user1@example.com", name: "User1")
25
+ @user_class.create(email: "user2@example.com", name: "User2")
26
+
27
+ users = @user_class.multiget("user1@example.com", "user2@example.com")
28
+
29
+ users.length == 2 && users.all? { |u| u.is_a?(@user_class) }
30
+ ensure
31
+ @user_class.destroy!("user1@example.com", "user2@example.com")
32
+ end
33
+
34
+ try "find_keys returns matching Redis keys" do
35
+ @user_class.create(email: "test@example.com", name: "Test")
36
+ keys = @user_class.find_keys
37
+
38
+ keys.any? { |key| key.include?("test@example.com") }
39
+ ensure
40
+ @user_class.destroy!("test@example.com")
41
+ end
@@ -0,0 +1,49 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test Horreum Redis commands
4
+ group "Horreum Commands"
5
+
6
+ setup do
7
+ @user_class = Class.new(Familia::Horreum) do
8
+ identifier :email
9
+ field :name
10
+ field :score
11
+ end
12
+ @user = @user_class.new(email: "test@example.com", name: "Test")
13
+ @user.save
14
+ end
15
+
16
+ try "hget/hset operations" do
17
+ @user.hset("name", "Updated")
18
+ @user.hget("name") == "Updated"
19
+ end
20
+
21
+ try "increment/decrement operations" do
22
+ @user.hset("score", "100")
23
+ @user.incr("score", 10)
24
+ @user.hget("score").to_i == 110 &&
25
+ @user.decr("score", 5) == 105
26
+ end
27
+
28
+ try "field existence and removal" do
29
+ @user.hset("temp_field", "value")
30
+ exists_before = @user.key?("temp_field")
31
+ @user.remove_field("temp_field")
32
+ exists_after = @user.key?("temp_field")
33
+
34
+ exists_before && !exists_after
35
+ end
36
+
37
+ try "bulk field operations" do
38
+ fields = @user.hkeys
39
+ values = @user.hvals
40
+ all_data = @user.hgetall
41
+
42
+ fields.is_a?(Array) &&
43
+ values.is_a?(Array) &&
44
+ all_data.is_a?(Hash)
45
+ end
46
+
47
+ cleanup do
48
+ @user&.delete!
49
+ end