familia 1.2.1 → 2.0.0.pre2

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 +4 -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 +16 -2
  11. data/Gemfile.lock +97 -36
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +192 -0
  15. data/familia.gemspec +10 -6
  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 -110
  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 +18 -13
  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} +7 -5
  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 -7
  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} +60 -59
  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} +21 -18
  88. data/try/models/datatype_base_try.rb +100 -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 +143 -46
  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,93 @@
1
+ # try/core/errors_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ # Test Familia error classes and exception handling
9
+
10
+ ## Familia::Problem is base error class
11
+ Familia::Problem.new.class.superclass
12
+ #=> RuntimeError
13
+
14
+ ## NoIdentifier error can be raised
15
+ begin
16
+ raise Familia::NoIdentifier, "Missing identifier"
17
+ rescue Familia::NoIdentifier => e
18
+ e.class
19
+ end
20
+ #=> Familia::NoIdentifier
21
+
22
+ ## NonUniqueKey error can be raised
23
+ begin
24
+ raise Familia::NonUniqueKey, "Duplicate key"
25
+ rescue Familia::NonUniqueKey => e
26
+ e.class
27
+ end
28
+ #=> Familia::NonUniqueKey
29
+
30
+ ## HighRiskFactor error stores value
31
+ begin
32
+ raise Familia::HighRiskFactor.new("dangerous_value")
33
+ rescue Familia::HighRiskFactor => e
34
+ e.value
35
+ end
36
+ #=> "dangerous_value"
37
+
38
+ ## HighRiskFactor error has custom message
39
+ begin
40
+ raise Familia::HighRiskFactor.new(123)
41
+ rescue Familia::HighRiskFactor => e
42
+ e.message.include?("High risk factor")
43
+ end
44
+ #=> true
45
+
46
+ ## NotConnected error stores URI
47
+ test_uri = URI.parse('redis://localhost:6379')
48
+ begin
49
+ raise Familia::NotConnected.new(test_uri)
50
+ rescue Familia::NotConnected => e
51
+ e.uri.to_s
52
+ end
53
+ #=> "redis://localhost"
54
+
55
+ ## NotConnected error has custom message
56
+ test_uri = URI.parse('redis://localhost:6379')
57
+ begin
58
+ raise Familia::NotConnected.new(test_uri)
59
+ rescue Familia::NotConnected => e
60
+ e.message.include?("No client for")
61
+ end
62
+ #> true
63
+
64
+ ## KeyNotFoundError stores key
65
+ begin
66
+ raise Familia::KeyNotFoundError.new("missing:key")
67
+ rescue Familia::KeyNotFoundError => e
68
+ e.key
69
+ end
70
+ #=> "missing:key"
71
+
72
+ ## KeyNotFoundError has custom message
73
+ begin
74
+ raise Familia::KeyNotFoundError.new("test:key")
75
+ rescue Familia::KeyNotFoundError => e
76
+ e.message.include?("Key not found in Redis")
77
+ end
78
+ #=> true
79
+
80
+ ## KeyNotFoundError has custom message again
81
+ raise Familia::KeyNotFoundError.new("test:key")
82
+ #=!> error.message.include?("Key not found in Redis")
83
+ #=!> error.class == Familia::KeyNotFoundError
84
+
85
+ ## All error classes inherit from Problem
86
+ [
87
+ Familia::NoIdentifier,
88
+ Familia::NonUniqueKey,
89
+ Familia::HighRiskFactor,
90
+ Familia::NotConnected,
91
+ Familia::KeyNotFoundError
92
+ ].all? { |klass| klass.superclass == Familia::Problem }
93
+ ##=> true
@@ -0,0 +1,26 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test core extensions
4
+ group "Core Extensions"
5
+
6
+ try "String time parsing with in_seconds" do
7
+ "60s".in_seconds == 60 &&
8
+ "5m".in_seconds == 300 &&
9
+ "2h".in_seconds == 7200 &&
10
+ "1d".in_seconds == 86400
11
+ end
12
+
13
+ try "Time::Units conversions" do
14
+ 1.second == 1 &&
15
+ 1.minute == 60 &&
16
+ 1.hour == 3600 &&
17
+ 1.day == 86400 &&
18
+ 1.week == 604800
19
+ end
20
+
21
+ try "Numeric extensions to_ms and to_bytes" do
22
+ 1000.to_ms == 1000 &&
23
+ 1.to_bytes == 1 &&
24
+ 1024.to_bytes == "1.0 KiB" &&
25
+ (1024 * 1024).to_bytes == "1.0 MiB"
26
+ end
@@ -1,28 +1,29 @@
1
+ # try/core/familia_extended_try.rb
1
2
 
2
3
  require 'time'
3
4
 
4
- require_relative '../lib/familia'
5
- require_relative './test_helpers'
5
+ require_relative '../../lib/familia'
6
+ require_relative '../helpers/test_helpers'
6
7
 
7
- ## Has all redistype relativess
8
- registered_types = Familia::RedisType.registered_types.keys
8
+ ## Has all datatype relativess
9
+ registered_types = Familia::DataType.registered_types.keys
9
10
  registered_types.collect(&:to_s).sort
10
11
  #=> ["counter", "hash", "hashkey", "list", "lock", "set", "sorted_set", "string", "zset"]
11
12
 
12
- ## Familia created class methods for redistype list class
13
+ ## Familia created class methods for datatype list class
13
14
  Familia::Horreum::ClassMethods.public_method_defined? :list?
14
15
  #=> true
15
16
 
16
- ## Familia created class methods for redistype list class
17
+ ## Familia created class methods for datatype list class
17
18
  Familia::Horreum::ClassMethods.public_method_defined? :list
18
19
  #=> true
19
20
 
20
- ## Familia created class methods for redistype list class
21
+ ## Familia created class methods for datatype list class
21
22
  Familia::Horreum::ClassMethods.public_method_defined? :lists
22
23
  #=> true
23
24
 
24
- ## A Familia object knows its redistype relatives
25
- Bone.redis_types.is_a?(Hash) && Bone.redis_types.has_key?(:owners)
25
+ ## A Familia object knows its datatype relatives
26
+ Bone.related_fields.is_a?(Hash) && Bone.related_fields.has_key?(:owners)
26
27
  #=> true
27
28
 
28
29
  ## A Familia object knows its lists
@@ -33,7 +34,7 @@ Bone.lists.size
33
34
  Bone.list? :owners
34
35
  #=> true
35
36
 
36
- ## A Familia object can get a specific redistype relatives def
37
+ ## A Familia object can get a specific datatype relatives def
37
38
  definition = Bone.list :owners
38
39
  definition.klass
39
40
  #=> Familia::List
@@ -1,17 +1,19 @@
1
+ # try/core/familia_try.rb
1
2
 
2
- require_relative '../lib/familia'
3
- require_relative './test_helpers'
3
+
4
+ require_relative '../../lib/familia'
5
+ require_relative '../helpers/test_helpers'
4
6
 
5
7
  #Familia.apiversion = 'v1'
6
8
 
7
9
 
8
10
  ## Check for help class
9
- Bone.redis_types.keys # consistent b/c hashes are ordered
11
+ Bone.related_fields.keys # consistent b/c hashes are ordered
10
12
  #=> [:owners, :tags, :metrics, :props, :value]
11
13
 
12
14
  ## Familia has a uri
13
- Familia.uri.class
14
- #=> URI::Redis
15
+ Familia.uri
16
+ #=:> URI::Generic
15
17
 
16
18
  ## Familia has a uri as a string
17
19
  Familia.uri.to_s
@@ -0,0 +1,68 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test Redis middleware components
4
+ group "Redis Middleware"
5
+
6
+ try "RedisLogger logs commands with timing" do
7
+ # Mock Redis client with middleware
8
+ class MockRedis
9
+ attr_reader :logged_commands
10
+
11
+ def initialize
12
+ @logged_commands = []
13
+ extend RedisLogger
14
+ end
15
+
16
+ def get(key)
17
+ log_command("GET", key) { "test_value" }
18
+ end
19
+
20
+ private
21
+
22
+ def log_command(cmd, *args)
23
+ start_time = Time.now
24
+ result = yield
25
+ duration = Time.now - start_time
26
+ @logged_commands << { command: cmd, args: args, duration: duration }
27
+ result
28
+ end
29
+ end
30
+
31
+ redis = MockRedis.new
32
+ result = redis.get("test_key")
33
+
34
+ result == "test_value" &&
35
+ redis.logged_commands.length == 1 &&
36
+ redis.logged_commands.first[:command] == "GET"
37
+ end
38
+
39
+ try "RedisCommandCounter tracks command metrics" do
40
+ # Mock counter implementation
41
+ counter = RedisCommandCounter.new
42
+
43
+ counter.increment("GET")
44
+ counter.increment("SET")
45
+ counter.increment("GET")
46
+
47
+ counter.count("GET") == 2 &&
48
+ counter.count("SET") == 1 &&
49
+ counter.total == 3
50
+ rescue NameError
51
+ # Skip if RedisCommandCounter not available
52
+ true
53
+ end
54
+
55
+ try "Command counting with count_commands utility" do
56
+ redis = Familia.redis
57
+
58
+ count = count_commands do
59
+ redis.set("test_key", "value")
60
+ redis.get("test_key")
61
+ redis.del("test_key")
62
+ end
63
+
64
+ count >= 3 # At least 3 commands executed
65
+ rescue NameError, NoMethodError
66
+ # Skip if count_commands not available
67
+ true
68
+ end
@@ -0,0 +1,39 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test Familia refinements
4
+ group "Familia Refinements"
5
+
6
+ using Familia::FlexibleHashAccess
7
+
8
+ try "FlexibleHashAccess allows string/symbol key interchange" do
9
+ hash = { name: "test", "email" => "test@example.com" }
10
+
11
+ hash[:name] == "test" &&
12
+ hash["name"] == "test" &&
13
+ hash[:email] == "test@example.com" &&
14
+ hash["email"] == "test@example.com"
15
+ end
16
+
17
+ try "LoggerTraceRefinement adds trace level when FAMILIA_TRACE enabled" do
18
+ old_env = ENV['FAMILIA_TRACE']
19
+ ENV['FAMILIA_TRACE'] = '1'
20
+
21
+ logger = Logger.new(STDOUT)
22
+ logger.respond_to?(:trace)
23
+ ensure
24
+ ENV['FAMILIA_TRACE'] = old_env
25
+ end
26
+
27
+ try "FAMILIA_TRACE environment control" do
28
+ old_env = ENV['FAMILIA_TRACE']
29
+
30
+ ENV['FAMILIA_TRACE'] = '1'
31
+ trace_enabled = ENV['FAMILIA_TRACE']
32
+
33
+ ENV['FAMILIA_TRACE'] = nil
34
+ trace_disabled = ENV['FAMILIA_TRACE']
35
+
36
+ trace_enabled == '1' && trace_disabled.nil?
37
+ ensure
38
+ ENV['FAMILIA_TRACE'] = old_env
39
+ end
@@ -0,0 +1,76 @@
1
+ # try/core/settings_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ # Test Familia configuration and settings management
9
+
10
+ ## Familia has default delim
11
+ Familia.delim
12
+ #=> ":"
13
+
14
+ ## Can set custom delim
15
+ Familia.delim('|')
16
+ Familia.delim
17
+ #=> "|"
18
+
19
+ ## Familia has default suffix
20
+ Familia.suffix
21
+ #=> :object
22
+
23
+ ## Can set custom suffix
24
+ Familia.suffix(:data)
25
+ Familia.suffix
26
+ #=> :data
27
+
28
+ ## Familia has default TTL
29
+ Familia.default_expiration
30
+ #=> 0
31
+
32
+ ## Can set default expiration value
33
+ Familia.default_expiration(3600)
34
+ Familia.default_expiration
35
+ #=> 3600.0
36
+
37
+ ## Familia has default database
38
+ Familia.logical_database
39
+ #=> nil
40
+
41
+ ## Can set database number
42
+ Familia.logical_database(2)
43
+ Familia.logical_database
44
+ #=> 2
45
+
46
+ ## Familia has default prefix
47
+ Familia.prefix
48
+ #=> nil
49
+
50
+ ## Can set custom prefix
51
+ Familia.prefix('app')
52
+ Familia.prefix
53
+ #=> "app"
54
+
55
+ ## default_suffix method returns suffix
56
+ Familia.default_suffix
57
+ #=> :data
58
+
59
+ ## Setting values with nil preserves current value
60
+ current_default_expiration = Familia.default_expiration
61
+ Familia.default_expiration(nil)
62
+ Familia.default_expiration
63
+ #=> 3600.0
64
+
65
+ ## Setting delim with nil preserves current value
66
+ current_delim = Familia.delim
67
+ Familia.delim(nil)
68
+ Familia.delim
69
+ #=> "|"
70
+
71
+ # Cleanup - restore defaults
72
+ Familia.delim(':')
73
+ Familia.suffix(:object)
74
+ Familia.default_expiration(0)
75
+ Familia.logical_database(nil)
76
+ Familia.prefix(nil)
@@ -0,0 +1,54 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ # Test Familia::Tools - key migration and utility functions
4
+ group "Familia::Tools"
5
+
6
+ try "move_keys across Redis instances" do
7
+ source_redis = Redis.new(db: 10)
8
+ dest_redis = Redis.new(db: 11)
9
+
10
+ # Setup test data
11
+ source_redis.set("test:key1", "value1")
12
+ source_redis.set("test:key2", "value2")
13
+
14
+ # Move keys
15
+ moved = Familia::Tools.move_keys(source_redis, dest_redis, "test:*")
16
+
17
+ moved == 2 &&
18
+ dest_redis.get("test:key1") == "value1" &&
19
+ !source_redis.exists?("test:key1")
20
+ ensure
21
+ source_redis&.flushdb
22
+ dest_redis&.flushdb
23
+ end
24
+
25
+ try "rename with transformation block" do
26
+ redis = Familia.redis
27
+ redis.set("old:key1", "value1")
28
+ redis.set("old:key2", "value2")
29
+
30
+ renamed = Familia::Tools.rename(redis, "old:*") { |key| key.gsub("old:", "new:") }
31
+
32
+ renamed == 2 &&
33
+ redis.get("new:key1") == "value1" &&
34
+ !redis.exists?("old:key1")
35
+ ensure
36
+ redis&.del("old:key1", "old:key2", "new:key1", "new:key2")
37
+ end
38
+
39
+ try "get_any retrieves values regardless of type" do
40
+ redis = Familia.redis
41
+ redis.set("string_key", "string_value")
42
+ redis.hset("hash_key", "field", "hash_value")
43
+ redis.lpush("list_key", "list_value")
44
+
45
+ string_val = Familia::Tools.get_any(redis, "string_key")
46
+ hash_val = Familia::Tools.get_any(redis, "hash_key")
47
+ list_val = Familia::Tools.get_any(redis, "list_key")
48
+
49
+ string_val == "string_value" &&
50
+ hash_val.is_a?(Hash) &&
51
+ list_val.is_a?(Array)
52
+ ensure
53
+ redis&.del("string_key", "hash_key", "list_key")
54
+ end
@@ -0,0 +1,189 @@
1
+ # try/core/utils_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ Familia.debug = false
7
+
8
+ # Test Familia utility methods and helpers
9
+
10
+ ##
11
+ ## ID Generation
12
+ ##
13
+
14
+ ## Familia.generate_id
15
+ Familia.respond_to?(:generate_id)
16
+ #=> true
17
+
18
+ ## Can generate a default base-36 ID
19
+ id = Familia.generate_id
20
+ [id.class, id.length > 10, id.match?(/^[a-z0-9]+$/)]
21
+ #=> [String, true, true]
22
+
23
+ ## Generated IDs are unique
24
+ [Familia.generate_id == Familia.generate_id]
25
+ #=> [false]
26
+
27
+ ## Can generate an ID with a custom base (hex)
28
+ hex_id = Familia.generate_id(16)
29
+ [hex_id.class, hex_id.length == 64, hex_id.match?(/^[a-f0-9]+$/)]
30
+ #=> [String, true, true]
31
+
32
+ ## Familia.generate_trace_id
33
+ Familia.respond_to?(:generate_trace_id)
34
+ #=> true
35
+
36
+ ## Can generate a default base-36 trace ID
37
+ trace_id = Familia.generate_trace_id
38
+ [trace_id.class, trace_id.length > 5, trace_id.length < 20]
39
+ #=> [String, true, true]
40
+
41
+ ## Can generate a trace ID with a custom base (hex)
42
+ hex_trace_id = Familia.generate_trace_id(16)
43
+ [hex_trace_id.class, hex_trace_id.length == 16]
44
+ #=> [String, true]
45
+
46
+ ## Familia.generate_hex_id
47
+ Familia.respond_to?(:generate_hex_id)
48
+ #=> true
49
+
50
+ ## Can generate a 256-bit hex ID
51
+ hex_id = Familia.generate_hex_id
52
+ [hex_id.class, hex_id.length == 64, hex_id.match?(/^[a-f0-9]+$/)]
53
+ #=> [String, true, true]
54
+
55
+ ## Familia.generate_hex_trace_id
56
+ Familia.respond_to?(:generate_hex_trace_id)
57
+ #=> true
58
+
59
+ ## Can generate a 64-bit hex trace ID
60
+ hex_trace_id = Familia.generate_hex_trace_id
61
+ [hex_trace_id.class, hex_trace_id.length == 16, hex_trace_id.match?(/^[a-f0-9]+$/)]
62
+ #=> [String, true, true]
63
+
64
+
65
+ ##
66
+ ## ID Shortening
67
+ ##
68
+
69
+ ## Familia.shorten_to_external_id
70
+ Familia.respond_to?(:shorten_to_external_id)
71
+ #=> true
72
+
73
+ ## Can shorten hex ID to external ID (128 bits)
74
+ hex_id = Familia.generate_hex_id
75
+ external_id = Familia.shorten_to_external_id(hex_id)
76
+ [external_id.class, external_id.length < hex_id.length]
77
+ #=> [String, true]
78
+
79
+ ## Can shorten hex ID to external ID with custom base (hex)
80
+ hex_id = Familia.generate_hex_id
81
+ hex_external_id = Familia.shorten_to_external_id(hex_id, base: 16)
82
+ [hex_external_id.class, hex_external_id.length == 32]
83
+ #=> [String, true]
84
+
85
+ ## Familia.shorten_to_trace_id
86
+ Familia.respond_to?(:shorten_to_trace_id)
87
+ #=> true
88
+
89
+ ## Can shorten hex ID to trace ID (64 bits)
90
+ hex_id = Familia.generate_hex_id
91
+ trace_id = Familia.shorten_to_trace_id(hex_id)
92
+ [trace_id.class, trace_id.length < hex_id.length]
93
+ #=> [String, true]
94
+
95
+ ## Can shorten hex ID to trace ID with custom base (hex)
96
+ hex_id = Familia.generate_hex_id
97
+ hex_trace_id = Familia.shorten_to_trace_id(hex_id, base: 16)
98
+ [hex_trace_id.class, hex_trace_id.length == 16]
99
+ #=> [String, true]
100
+
101
+ ## Shortened IDs are deterministic
102
+ hex_id = Familia.generate_hex_id
103
+ id1 = Familia.shorten_to_external_id(hex_id)
104
+ id2 = Familia.shorten_to_external_id(hex_id)
105
+ id1 == id2
106
+ #=> true
107
+
108
+ ##
109
+ ## Key helpers
110
+ ##
111
+ Familia.delim(':') # Ensure default
112
+
113
+ ## Can join values with delimiter
114
+ result = Familia.join('user', '123', 'sessions')
115
+ result
116
+ #=> "user:123:sessions"
117
+
118
+ ## Can join with custom delimiter
119
+ original_delim = Familia.delim
120
+ Familia.delim('|')
121
+ result = Familia.join('a', 'b', 'c')
122
+ [result, Familia.delim]
123
+ #=> ["a|b|c", "|"]
124
+
125
+ ## Can split values with custom delimiter
126
+ parts = Familia.split('a|b|c')
127
+ parts
128
+ #=> ["a", "b", "c"]
129
+
130
+ ## Resets delimiter for remaining tests
131
+ Familia.delim(':')
132
+ #=> ":"
133
+
134
+ ## Can split with default delimiter after reset
135
+ parts = Familia.split('user:123:data')
136
+ parts
137
+ #=> ["user", "123", "data"]
138
+
139
+ ## Can create dbkey with default delimiter
140
+ key = Familia.dbkey('v1', 'customer', 'email')
141
+ key
142
+ #=> "v1:customer:email"
143
+
144
+ ##
145
+ ## Other Utilities
146
+ ##
147
+
148
+ ## qstamp returns integer timestamp by default
149
+ stamp = Familia.qstamp
150
+ stamp.class
151
+ #=> Integer
152
+
153
+ ## qstamp with pattern returns formatted string
154
+ formatted = Familia.qstamp(3600, pattern: '%Y%m%d%H')
155
+ [formatted.class, formatted.length == 10]
156
+ #=> [String, true]
157
+
158
+ ## qstamp works with custom time
159
+ test_time = Time.utc(2023, 6, 15, 14, 30, 0)
160
+ custom_stamp = Familia.qstamp(3600, time: test_time)
161
+ Time.at(custom_stamp).utc.hour
162
+ #=> 14
163
+
164
+ ## distinguisher handles basic types
165
+ str_result = Familia.distinguisher('test')
166
+ int_result = Familia.distinguisher(123)
167
+ sym_result = Familia.distinguisher(:symbol)
168
+ [str_result, int_result, sym_result]
169
+ #=> ["test", "123", "symbol"]
170
+
171
+ ## distinguisher raises error for high-risk types with strict mode
172
+ begin
173
+ Familia.distinguisher(true, strict_values: true)
174
+ rescue Familia::HighRiskFactor => e
175
+ e.class
176
+ end
177
+ #=> Familia::HighRiskFactor
178
+
179
+ ## distinguisher allows high-risk types with non-strict mode
180
+ result = Familia.distinguisher(false, strict_values: false)
181
+ result
182
+ #=> "false"
183
+
184
+ # Cleanup - restore defaults, leave nothing but footprints
185
+ Familia.delim(':')
186
+ Familia.suffix(:object)
187
+ Familia.default_expiration(0)
188
+ Familia.logical_database(nil)
189
+ Familia.prefix(nil)
@@ -1,5 +1,7 @@
1
- require_relative '../lib/familia'
2
- require_relative './test_helpers'
1
+ # try/datatypes/boolean_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
3
5
 
4
6
  Familia.debug = false
5
7
 
@@ -0,0 +1,69 @@
1
+ # try/datatypes/base_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require_relative '../helpers/test_helpers'
5
+
6
+ @limiter1 = Limiter.new :requests
7
+
8
+ ## Database Types are unique per instance of a Familia class
9
+ @a = Bone.new 'atoken1', :name1
10
+ @b = Bone.new 'atoken2', :name2
11
+ p [@a.object_id, @b.object_id]
12
+ p [@a.owners.parent.class, @b.owners.parent.class]
13
+ p [@a.owners.parent.object_id, @b.owners.parent.object_id]
14
+ p [@a.owners.dbkey, @b.owners.dbkey]
15
+ p [@a.token, @b.token]
16
+ p [@a.name, @b.name]
17
+ @a.owners.dbkey.eql?(@b.owners.dbkey)
18
+ #=> false
19
+
20
+ ## Database Types are frozen
21
+ @a.owners.frozen?
22
+ #=> true
23
+
24
+ ## Limiter#qstamp
25
+ @limiter1.counter.qstamp(10.minutes, '%H:%M', 1302468980)
26
+ ##=> '20:50'
27
+
28
+ ## Database Types can be stored to quantized stamp suffix
29
+ @limiter1.counter.dbkey
30
+ ##=> "v1:limiter:requests:counter:20:50"
31
+
32
+ ## Limiter#qstamp as a number
33
+ @limiter2 = Limiter.new :requests
34
+ p [@limiter1.default_expiration, @limiter2.default_expiration]
35
+ p [@limiter1.counter.parent.default_expiration, @limiter2.counter.parent.default_expiration]
36
+ @limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1302468980)
37
+ #=> 1302468600
38
+
39
+ ## Database Types can be stored to quantized numeric suffix. This
40
+ ## tryouts is disabled b/c `DataType#dbkey` takes no args
41
+ ## and relies on the `class Limiter` definition in test_helpers.rb
42
+ ## for the `:quantize` option. The quantized suffix for the Limiter
43
+ ## class is `'%H:%M'` so its dbkeys will always look like that.
44
+ @limiter2.counter.dbkey
45
+ ##=> "v1:limiter:requests:counter:1302468600"
46
+
47
+ ## Increment counter
48
+ @limiter1.counter.delete!
49
+ @limiter1.counter.increment
50
+ #=> 1
51
+
52
+ ## Check counter default_expiration
53
+ @limiter1.counter.default_expiration
54
+ #=> 3600.0
55
+
56
+ ## Check limiter default_expiration
57
+ @limiter1.default_expiration
58
+ #=> 1800.0
59
+
60
+ ## Check default_expiration for a different instance
61
+ ## (this exists to make sure options are cloned for each instance)
62
+ @limiter3 = Limiter.new :requests
63
+ @limiter3.counter.default_expiration
64
+ #=> 3600.0
65
+
66
+ ## Check current_expiration
67
+ sleep 1 # Database default_expirations are in seconds so we can't wait any less time than this (without mocking)
68
+ @limiter1.counter.current_expiration
69
+ #=> 3600-1