familia 2.0.0.pre10 → 2.0.0.pre13

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +2 -3
  3. data/CHANGELOG.rst +507 -0
  4. data/CLAUDE.md +5 -55
  5. data/Gemfile +1 -6
  6. data/Gemfile.lock +13 -7
  7. data/changelog.d/README.md +45 -34
  8. data/changelog.d/scriv.ini +5 -0
  9. data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
  10. data/docs/archive/FAMILIA_UPDATE.md +1 -1
  11. data/docs/archive/README.md +15 -19
  12. data/docs/guides/Feature-System-Autoloading.md +228 -0
  13. data/docs/guides/Home.md +1 -1
  14. data/docs/guides/Implementation-Guide.md +1 -1
  15. data/docs/guides/relationships-methods.md +1 -1
  16. data/docs/guides/time-utilities.md +221 -0
  17. data/docs/migrating/.gitignore +2 -0
  18. data/docs/migrating/v2.0.0-pre.md +84 -0
  19. data/docs/migrating/v2.0.0-pre11.md +253 -0
  20. data/docs/migrating/v2.0.0-pre12.md +306 -0
  21. data/docs/migrating/v2.0.0-pre13.md +329 -0
  22. data/docs/migrating/v2.0.0-pre5.md +110 -0
  23. data/docs/migrating/v2.0.0-pre6.md +154 -0
  24. data/docs/migrating/v2.0.0-pre7.md +222 -0
  25. data/docs/overview.md +6 -7
  26. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  27. data/examples/autoloader/mega_customer/safe_dump_fields.rb +6 -0
  28. data/examples/autoloader/mega_customer.rb +17 -0
  29. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  30. data/examples/{relationships_basic.rb → relationships.rb} +2 -3
  31. data/examples/safe_dump.rb +281 -0
  32. data/familia.gemspec +5 -4
  33. data/lib/familia/autoloader.rb +53 -0
  34. data/lib/familia/base.rb +57 -0
  35. data/lib/familia/data_type.rb +4 -0
  36. data/lib/familia/encryption/encrypted_data.rb +4 -4
  37. data/lib/familia/encryption/manager.rb +6 -4
  38. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  39. data/lib/familia/encryption.rb +1 -1
  40. data/lib/familia/errors.rb +5 -0
  41. data/lib/familia/features/autoloadable.rb +113 -0
  42. data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
  43. data/lib/familia/features/expiration.rb +4 -0
  44. data/lib/familia/features/external_identifier.rb +310 -0
  45. data/lib/familia/features/object_identifier.rb +307 -0
  46. data/lib/familia/features/quantization.rb +5 -0
  47. data/lib/familia/features/safe_dump.rb +74 -73
  48. data/lib/familia/features.rb +109 -17
  49. data/lib/familia/field_type.rb +2 -0
  50. data/lib/familia/horreum/core/serialization.rb +3 -3
  51. data/lib/familia/horreum/subclass/definition.rb +50 -7
  52. data/lib/familia/horreum.rb +2 -0
  53. data/lib/familia/json_serializer.rb +70 -0
  54. data/lib/familia/logging.rb +12 -10
  55. data/lib/familia/refinements/logger_trace.rb +57 -0
  56. data/lib/familia/refinements/snake_case.rb +40 -0
  57. data/lib/familia/refinements/time_utils.rb +248 -0
  58. data/lib/familia/refinements.rb +3 -49
  59. data/lib/familia/secure_identifier.rb +51 -75
  60. data/lib/familia/utils.rb +2 -0
  61. data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
  62. data/lib/familia/validation.rb +1 -1
  63. data/lib/familia/verifiable_identifier.rb +162 -0
  64. data/lib/familia/version.rb +1 -1
  65. data/lib/familia.rb +15 -2
  66. data/try/core/autoloader_try.rb +112 -0
  67. data/try/core/extensions_try.rb +38 -21
  68. data/try/core/familia_extended_try.rb +4 -3
  69. data/try/core/secure_identifier_try.rb +47 -18
  70. data/try/core/time_utils_try.rb +130 -0
  71. data/try/core/verifiable_identifier_try.rb +171 -0
  72. data/try/data_types/datatype_base_try.rb +3 -2
  73. data/try/features/autoloadable/autoloadable_try.rb +61 -0
  74. data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
  75. data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
  76. data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
  77. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  78. data/try/features/feature_improvements_try.rb +127 -0
  79. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  80. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  81. data/try/features/real_feature_integration_try.rb +8 -7
  82. data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
  83. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  84. data/try/helpers/test_helpers.rb +41 -17
  85. data/try/integration/cross_component_try.rb +3 -1
  86. metadata +61 -26
  87. data/CHANGELOG.md +0 -184
  88. data/changelog.d/fragments/.keep +0 -0
  89. data/changelog.d/template.md.j2 +0 -29
  90. data/lib/familia/core_ext.rb +0 -135
  91. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  92. data/lib/familia/features/external_identifiers.rb +0 -111
  93. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  94. data/lib/familia/features/object_identifiers.rb +0 -194
  95. data/setup.cfg +0 -12
@@ -2,13 +2,14 @@
2
2
 
3
3
  require 'securerandom'
4
4
 
5
+ # Provides a suite of tools for generating and manipulating cryptographically
6
+ # secure identifiers in various formats and lengths.
5
7
  module Familia
6
8
  module SecureIdentifier
7
-
8
9
  # Generates a 256-bit cryptographically secure hexadecimal identifier.
9
10
  #
10
11
  # @return [String] A 64-character hex string representing 256 bits of entropy.
11
- # @security Provides ~10^77 possible values, far exceeding UUID4's 128 bits.
12
+ # @security Provides ~10^77 possible values, far exceeding UUIDv4's 128 bits.
12
13
  def generate_hex_id
13
14
  SecureRandom.hex(32)
14
15
  end
@@ -26,13 +27,6 @@ module Familia
26
27
  #
27
28
  # @param base [Integer] The base for encoding the output string (2-36, default: 36).
28
29
  # @return [String] A secure identifier.
29
- #
30
- # @example Generate a 256-bit ID in base-36 (default)
31
- # generate_id # => "25nkfebno45yy36z47ffxef2a7vpg4qk06ylgxzwgpnz4q3os4"
32
- #
33
- # @example Generate a 256-bit ID in base-16 (hexadecimal)
34
- # generate_id(16) # => "568bdb582bc5042bf435d3f126cf71593981067463709c880c91df1ad9777a34"
35
- #
36
30
  def generate_id(base = 36)
37
31
  target_length = SecureIdentifier.min_length_for_bits(256, base)
38
32
  generate_hex_id.to_i(16).to_s(base).rjust(target_length, '0')
@@ -43,87 +37,69 @@ module Familia
43
37
  #
44
38
  # @param base [Integer] The base for encoding the output string (2-36, default: 36).
45
39
  # @return [String] A secure short identifier.
46
- #
47
- # @example Generate a 64-bit short ID in base-36 (default)
48
- # generate_trace_id # => "lh7uap704unf"
49
- #
50
- # @example Generate a 64-bit short ID in base-16 (hexadecimal)
51
- # generate_trace_id(16) # => "94cf9f8cfb0eb692"
52
- #
53
40
  def generate_trace_id(base = 36)
54
41
  target_length = SecureIdentifier.min_length_for_bits(64, base)
55
42
  generate_hex_trace_id.to_i(16).to_s(base).rjust(target_length, '0')
56
43
  end
57
44
 
58
- # Truncates a 256-bit hexadecimal ID to 64 bits and encodes it in a given base.
59
- # These short, deterministic IDs are useful for secure logging. By inputting the
60
- # full hexadecimal string, you can generate a consistent short ID that allows
61
- # tracking an entity through logs without exposing the entity's full identifier..
45
+ # Creates a deterministic 64-bit trace identifier from a longer hex ID.
62
46
  #
63
- # @param hex_id [String] A 64-character hexadecimal string (representing 256 bits).
64
- # @param base [Integer] The base for encoding the output string (2-36, default: 36).
65
- # @return [String] A 64-bit identifier, encoded in the specified base.
47
+ # This is a convenience method for `truncate_hex(hex_id, bits: 64)`.
48
+ # Useful for creating short, consistent IDs for logging and tracing.
49
+ #
50
+ # @param (see #truncate_hex)
51
+ # @return (see #truncate_hex)
66
52
  def shorten_to_trace_id(hex_id, base: 36)
67
- target_length = SecureIdentifier.min_length_for_bits(64, base)
68
- truncated = hex_id.to_i(16) >> (256 - 64) # Always 64 bits
69
- truncated.to_s(base).rjust(target_length, '0')
53
+ truncate_hex(hex_id, bits: 64, base: base)
70
54
  end
71
55
 
72
- # Truncates a 256-bit hexadecimal ID to 128 bits and encodes it in a given base.
73
- # This function takes the most significant bits from the hex string to maintain
74
- # randomness while creating a shorter, deterministic identifier that's safe for
75
- # outdoor use.
76
- #
77
- # @param hex_id [String] A 64-character hexadecimal string (representing 256 bits).
78
- # @param base [Integer] The base for encoding the output string (2-36, default: 36).
79
- # @return [String] A 128-bit identifier, encoded in the specified base.
80
- #
81
- # @example Create a shorter external ID from a full 256-bit internal ID
82
- # hex_id = generate_hex_id
83
- # external_id = shorten_to_external_id(hex_id)
84
- #
85
- # @note This is useful for creating shorter, public-facing IDs from secure internal ones.
86
- # @security Truncation preserves the cryptographic properties of the most significant bits.
87
- def shorten_to_external_id(hex_id, base: 36)
88
- target_length = SecureIdentifier.min_length_for_bits(128, base)
89
- truncated = hex_id.to_i(16) >> (256 - 128) # Always 128 bits
90
- truncated.to_s(base).rjust(target_length, '0')
91
- end
56
+ # Deterministically truncates a hexadecimal ID to a specified bit length.
57
+ #
58
+ # This function preserves the most significant bits of the input `hex_id` to
59
+ # create a shorter, yet still random-looking, identifier.
60
+ #
61
+ # @param hex_id [String] The input hexadecimal string.
62
+ # @param bits [Integer] The desired output bit length (e.g., 128, 64). Defaults to 128.
63
+ # @param base [Integer] The numeric base for the output string (2-36). Defaults to 36.
64
+ # @return [String] A new, shorter identifier in the specified base.
65
+ # @raise [ArgumentError] if `hex_id` is not a valid hex string, or if `input_bits`
66
+ # is less than the desired output `bits`.
67
+ def truncate_hex(hex_id, bits: 128, base: 36)
68
+ target_length = SecureIdentifier.min_length_for_bits(bits, base)
69
+ input_bits = hex_id.length * 4
92
70
 
93
- # Calculate minimum string length to represent N bits in given base
94
- #
95
- # When generating random IDs, we need to know how many characters are required
96
- # to represent a certain amount of entropy. This ensures consistent ID lengths.
97
- #
98
- # Formula: ceil(bits * log(2) / log(base))
99
- #
100
- # @example Common usage with SecureRandom
101
- # SecureRandom.hex(32) # 32 bytes = 256 bits = 64 hex chars
102
- # SecureRandom.hex(16) # 16 bytes = 128 bits = 32 hex chars
103
- #
104
- # @example Using the method
105
- # min_length_for_bits(256, 16) # => 64 (hex)
106
- # min_length_for_bits(256, 36) # => 50 (base36)
107
- # min_length_for_bits(128, 10) # => 39 (decimal)
71
+ unless hex_id.match?(/\A[0-9a-fA-F]+\z/)
72
+ raise ArgumentError, "Invalid hexadecimal string: #{hex_id}"
73
+ end
108
74
 
109
- # Fast lookup for hex (base 16) - our most common case
110
- # Avoids calculation overhead for 99% of ID generation
111
- HEX_LENGTHS = {
112
- 256 => 64, # SHA-256 equivalent entropy
113
- 128 => 32, # UUID equivalent entropy
114
- 64 => 16, # Compact ID
115
- }.freeze
75
+ if input_bits < bits
76
+ raise ArgumentError, "Input bits (#{input_bits}) cannot be less than desired output bits (#{bits})."
77
+ end
116
78
 
117
- # Get minimum character length needed to encode `bits` of entropy in `base`
79
+ # Truncate by right-shifting to keep the most significant bits
80
+ truncated_int = hex_id.to_i(16) >> (input_bits - bits)
81
+ truncated_int.to_s(base).rjust(target_length, '0')
82
+ end
83
+
84
+ # Calculates the minimum string length required to represent a given number of
85
+ # bits in a specific numeric base.
86
+ #
87
+ # @private
118
88
  #
119
- # @param bits [Integer] Number of bits of entropy needed
120
- # @param base [Integer] Numeric base (2-36)
121
- # @return [Integer] Minimum string length required
89
+ # @param bits [Integer] The number of bits of entropy.
90
+ # @param base [Integer] The numeric base (2-36).
91
+ # @return [Integer] The minimum string length required.
122
92
  def self.min_length_for_bits(bits, base)
123
- return HEX_LENGTHS[bits] if base == 16 && HEX_LENGTHS.key?(bits)
93
+ # Fast lookup for hex (base 16) - our most common case
94
+ hex_lengths = {
95
+ 256 => 64, # SHA-256 equivalent entropy
96
+ 128 => 32, # UUID equivalent entropy
97
+ 64 => 16, # Compact ID
98
+ }.freeze
99
+ return hex_lengths[bits] if base == 16 && hex_lengths.key?(bits)
124
100
 
125
- @length_cache ||= {}
126
- @length_cache[[bits, base]] ||= (bits * Math.log(2) / Math.log(base)).ceil
101
+ @min_length_for_bits_cache ||= {}
102
+ @min_length_for_bits_cache[[bits, base]] ||= (bits * Math.log(2) / Math.log(base)).ceil
127
103
  end
128
104
  end
129
105
  end
data/lib/familia/utils.rb CHANGED
@@ -6,6 +6,8 @@ module Familia
6
6
  #
7
7
  module Utils
8
8
 
9
+ using Familia::Refinements::TimeUtils
10
+
9
11
  # Joins array elements with Familia delimiter
10
12
  # @param val [Array] elements to join
11
13
  # @return [String] joined string
@@ -1,4 +1,4 @@
1
- # lib/familia/validation/test_helpers.rb
1
+ # lib/familia/validation/validation_helpers.rb
2
2
 
3
3
  module Familia
4
4
  module Validation
@@ -7,7 +7,7 @@ module Familia
7
7
  # and automatic setup/cleanup for command validation tests.
8
8
  #
9
9
  # @example Basic usage in a try file
10
- # require_relative '../validation/test_helpers'
10
+ # require_relative '../validation/validation_helpers'
11
11
  # extend Familia::Validation::TestHelpers
12
12
  #
13
13
  # ## User save should execute expected Redis commands
@@ -51,7 +51,7 @@
51
51
  require_relative 'validation/command_recorder'
52
52
  require_relative 'validation/expectations'
53
53
  require_relative 'validation/validator'
54
- require_relative 'validation/test_helpers'
54
+ require_relative 'validation/validation_helpers'
55
55
 
56
56
  module Familia
57
57
  module Validation
@@ -0,0 +1,162 @@
1
+ # lib/familia/verifiable_identifier.rb
2
+
3
+ require 'openssl'
4
+ require_relative 'secure_identifier'
5
+
6
+ module Familia
7
+ # Creates and verifies identifiers that contain an embedded HMAC signature,
8
+ # allowing for stateless verification of an identifier's authenticity.
9
+ module VerifiableIdentifier
10
+ # By extending SecureIdentifier, we gain access to its instance methods
11
+ # (like generate_hex_id) as class methods on this module.
12
+ extend Familia::SecureIdentifier
13
+
14
+ # The secret key for HMAC generation, loaded from an environment variable.
15
+ #
16
+ # This key is the root of trust for verifying identifier authenticity. It must be
17
+ # a long, random, and cryptographically strong string.
18
+ #
19
+ # @!attribute [r] SECRET_KEY
20
+ # @return [String] The secret key.
21
+ #
22
+ # @note Security Considerations:
23
+ # - **Secrecy:** This key MUST be kept secret and secure, just like a database
24
+ # password or API key. Do not commit it to version control.
25
+ # - **Consistency:** All running instances of your application must use the
26
+ # exact same key, otherwise verification will fail across different servers.
27
+ # - **Rotation:** If this key is ever compromised, it must be rotated. Be
28
+ # aware that rotating the key will invalidate all previously generated
29
+ # verifiable identifiers.
30
+ #
31
+ # @example Generating and Setting the Key
32
+ # 1. Generate a new secure key in your terminal:
33
+ # $ openssl rand -hex 32
34
+ # > cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d
35
+ #
36
+ # 2. Set it as an environment variable in your production environment:
37
+ # export VERIFIABLE_ID_HMAC_SECRET="cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d"
38
+ #
39
+ SECRET_KEY = ENV.fetch('VERIFIABLE_ID_HMAC_SECRET', 'cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d')
40
+
41
+ # The length of the random part of the ID in hex characters (256 bits).
42
+ RANDOM_HEX_LENGTH = 64
43
+ # The length of the HMAC tag in hex characters (64 bits).
44
+ # 64 bits is strong enough to prevent forgery (1 in 18 quintillion chance).
45
+ TAG_HEX_LENGTH = 16
46
+
47
+ # Generates a verifiable, base-36 encoded identifier.
48
+ #
49
+ # The final identifier contains a 256-bit random component and a 64-bit
50
+ # authentication tag.
51
+ #
52
+ # @param base [Integer] The base for encoding the output string.
53
+ # @return [String] A verifiable, signed identifier.
54
+ def self.generate_verifiable_id(base_or_scope = nil, scope: nil, base: 36)
55
+ # Handle backward compatibility with positional base argument
56
+ if base_or_scope.is_a?(Integer)
57
+ base = base_or_scope
58
+ # scope remains as passed in keyword argument
59
+ elsif base_or_scope.is_a?(String) || base_or_scope.nil?
60
+ scope = base_or_scope if scope.nil?
61
+ # base remains as passed in keyword argument or default
62
+ end
63
+
64
+ # Re-use generate_hex_id from the SecureIdentifier module.
65
+ random_hex = generate_hex_id
66
+ tag_hex = generate_tag(random_hex, scope: scope)
67
+
68
+ combined_hex = random_hex + tag_hex
69
+
70
+ # Re-use the min_length_for_bits helper from the SecureIdentifier module.
71
+ total_bits = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH) * 4
72
+ target_length = Familia::SecureIdentifier.min_length_for_bits(total_bits, base)
73
+
74
+ combined_hex.to_i(16).to_s(base).rjust(target_length, '0')
75
+ end
76
+
77
+ # Verifies the authenticity of a given identifier using a timing-safe comparison.
78
+ #
79
+ # @param verifiable_id [String] The identifier string to check.
80
+ # @param base [Integer] The base of the input string.
81
+ # @return [Boolean] True if the identifier is authentic, false otherwise.
82
+ def self.verified_identifier?(verifiable_id, base_or_scope = nil, scope: nil, base: 36)
83
+ # Handle backward compatibility with positional base argument
84
+ if base_or_scope.is_a?(Integer)
85
+ base = base_or_scope
86
+ # scope remains as passed in keyword argument
87
+ elsif base_or_scope.is_a?(String) || base_or_scope.nil?
88
+ scope = base_or_scope if scope.nil?
89
+ # base remains as passed in keyword argument or default
90
+ end
91
+
92
+ return false unless plausible_identifier?(verifiable_id, base)
93
+
94
+ expected_hex_length = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH)
95
+ combined_hex = verifiable_id.to_i(base).to_s(16).rjust(expected_hex_length, '0')
96
+
97
+ random_part = combined_hex[0...RANDOM_HEX_LENGTH]
98
+ tag_part = combined_hex[RANDOM_HEX_LENGTH..]
99
+
100
+ expected_tag = generate_tag(random_part, scope: scope)
101
+ OpenSSL.secure_compare(expected_tag, tag_part)
102
+ end
103
+
104
+ # Checks if an identifier is plausible (correct format and length) without
105
+ # performing cryptographic verification.
106
+ #
107
+ # This can be used as a fast pre-flight check to reject obviously
108
+ # malformed identifiers.
109
+ #
110
+ # @param identifier_str [String] The identifier string to check.
111
+ # @param base [Integer] The base of the input string.
112
+ # @return [Boolean] True if the identifier has a valid format, false otherwise.
113
+ def self.plausible_identifier?(identifier_str, base = 36)
114
+ return false unless identifier_str.is_a?(::String)
115
+
116
+ # 1. Check length
117
+ total_bits = (RANDOM_HEX_LENGTH + TAG_HEX_LENGTH) * 4
118
+ expected_length = Familia::SecureIdentifier.min_length_for_bits(total_bits, base)
119
+ return false unless identifier_str.length == expected_length
120
+
121
+ # 2. Check character set
122
+ # The most efficient way to check for invalid characters is to attempt
123
+ # conversion and rescue the error.
124
+ Integer(identifier_str, base)
125
+ true
126
+ rescue ArgumentError
127
+ false
128
+ end
129
+
130
+ class << self
131
+ private
132
+
133
+ # Generates the HMAC tag for a given message.
134
+ # @private
135
+ def generate_tag(message, scope: nil)
136
+ # Include scope in HMAC calculation for domain separation if provided.
137
+ # The scope parameter enables creating cryptographically isolated identifier
138
+ # namespaces (e.g., per-domain, per-tenant, per-application) while maintaining
139
+ # all security properties of the base system.
140
+ #
141
+ # Security considerations for scope values:
142
+ # - Any string content is cryptographically safe (HMAC handles arbitrary input)
143
+ # - No length restrictions (short scopes like "a" or long scopes work equally well)
144
+ # - UTF-8 encoding is handled consistently
145
+ # - Empty string "" vs nil produce different identifiers (intentional for security)
146
+ # - Different scope values guarantee different identifier spaces
147
+ #
148
+ # Examples of scope usage:
149
+ # - Customer isolation: scope: "tenant:#{tenant_id}"
150
+ # - Environment separation: scope: "production" vs scope: "staging"
151
+ # - Domain scoping: scope: "example.com"
152
+ # - Application scoping: scope: "#{app_name}:#{version}"
153
+ hmac_input = scope ? "#{message}:scope:#{scope}" : message
154
+
155
+ digest = OpenSSL::Digest.new('sha256')
156
+ hmac = OpenSSL::HMAC.hexdigest(digest, SECRET_KEY, hmac_input)
157
+ # Truncate to the desired length for the tag.
158
+ hmac[0...TAG_HEX_LENGTH]
159
+ end
160
+ end
161
+ end
162
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Familia
4
4
  # Version information for the Familia
5
- VERSION = '2.0.0.pre10'.freeze unless defined?(Familia::VERSION)
5
+ VERSION = '2.0.0.pre13'.freeze unless defined?(Familia::VERSION)
6
6
  end
data/lib/familia.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # lib/familia.rb
2
2
 
3
- require 'json'
3
+ require 'oj'
4
4
  require 'redis'
5
5
  require 'uri/valkey'
6
6
  require 'connection_pool'
7
7
 
8
- require_relative 'familia/core_ext'
8
+ # OJ configuration is handled internally by Familia::JsonSerializer
9
+
9
10
  require_relative 'familia/refinements'
10
11
  require_relative 'familia/errors'
11
12
  require_relative 'familia/version'
@@ -71,6 +72,7 @@ module Familia
71
72
  require_relative 'familia/connection'
72
73
  require_relative 'familia/settings'
73
74
  require_relative 'familia/utils'
75
+ require_relative 'familia/json_serializer'
74
76
 
75
77
  extend SecureIdentifier
76
78
  extend Connection
@@ -80,7 +82,18 @@ module Familia
80
82
  end
81
83
 
82
84
  require_relative 'familia/base'
85
+ require_relative 'familia/features/autoloadable'
83
86
  require_relative 'familia/features'
84
87
  require_relative 'familia/data_type'
85
88
  require_relative 'familia/horreum'
86
89
  require_relative 'familia/encryption'
90
+
91
+ # Ensure JSON constant is available for backward compatibility with existing code
92
+ # This approach is safer than monkey-patching core classes globally
93
+ begin
94
+ require 'json'
95
+ rescue LoadError
96
+ # If json gem is not available, define a minimal JSON constant
97
+ # that delegates to Familia::JsonSerializer for compatibility
98
+ JSON = Familia::JsonSerializer
99
+ end
@@ -0,0 +1,112 @@
1
+ # try/core/autoloader_try.rb
2
+
3
+ require_relative '../../lib/familia'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+
7
+ # Create test directory structure for Autoloader testing
8
+ @test_dir = Dir.mktmpdir('familia_autoloader_test')
9
+ @features_dir = File.join(@test_dir, 'features')
10
+ @test_file1 = File.join(@features_dir, 'test_feature1.rb')
11
+ @test_file2 = File.join(@features_dir, 'test_feature2.rb')
12
+ @excluded_file = File.join(@features_dir, 'autoloader.rb')
13
+
14
+ # Create directory structure
15
+ FileUtils.mkdir_p(@features_dir)
16
+
17
+ # Write test files
18
+ File.write(@test_file1, <<~RUBY)
19
+ # Test feature file 1
20
+ $test_feature1_loaded = true
21
+ RUBY
22
+
23
+ File.write(@test_file2, <<~RUBY)
24
+ # Test feature file 2
25
+ $test_feature2_loaded = true
26
+ RUBY
27
+
28
+ File.write(@excluded_file, <<~RUBY)
29
+ # This should be excluded
30
+ $autoloader_file_loaded = true
31
+ RUBY
32
+
33
+ ## Test that Familia::Autoloader exists and is a module
34
+ Familia::Autoloader.is_a?(Module)
35
+ #=> true
36
+
37
+ ## Test that autoload_files class method exists
38
+ Familia::Autoloader.respond_to?(:autoload_files)
39
+ #=> true
40
+
41
+ ## Test that included class method exists
42
+ Familia::Autoloader.respond_to?(:included)
43
+ #=> true
44
+
45
+ ## Test autoload_files with single pattern
46
+ $test_feature1_loaded = false
47
+ $test_feature2_loaded = false
48
+ $autoloader_file_loaded = false
49
+
50
+ Familia::Autoloader.autoload_files(File.join(@features_dir, '*.rb'))
51
+ $test_feature1_loaded && $test_feature2_loaded
52
+ #=> true
53
+
54
+ ## Test that autoload_files respects exclusions (using fresh files)
55
+ @exclude_test_dir = Dir.mktmpdir('familia_autoloader_exclude_test')
56
+ @exclude_features_dir = File.join(@exclude_test_dir, 'features')
57
+ @include_file = File.join(@exclude_features_dir, 'include_me.rb')
58
+ @exclude_file = File.join(@exclude_features_dir, 'autoloader.rb')
59
+
60
+ FileUtils.mkdir_p(@exclude_features_dir)
61
+ File.write(@include_file, '$include_me_loaded = true')
62
+ File.write(@exclude_file, '$exclude_me_loaded = true')
63
+
64
+ $include_me_loaded = false
65
+ $exclude_me_loaded = false
66
+
67
+ Familia::Autoloader.autoload_files(
68
+ File.join(@exclude_features_dir, '*.rb'),
69
+ exclude: ['autoloader.rb']
70
+ )
71
+
72
+ # Should load include file but not the excluded one
73
+ $include_me_loaded && !$exclude_me_loaded
74
+ #=> true
75
+
76
+ ## Test autoload_files with array of patterns (using fresh files)
77
+ @pattern_test_dir = Dir.mktmpdir('familia_autoloader_pattern_test')
78
+ @pattern_dir1 = File.join(@pattern_test_dir, 'dir1')
79
+ @pattern_dir2 = File.join(@pattern_test_dir, 'dir2')
80
+ @pattern_file1 = File.join(@pattern_dir1, 'file1.rb')
81
+ @pattern_file2 = File.join(@pattern_dir2, 'file2.rb')
82
+
83
+ FileUtils.mkdir_p(@pattern_dir1)
84
+ FileUtils.mkdir_p(@pattern_dir2)
85
+ File.write(@pattern_file1, '$pattern1_loaded = true')
86
+ File.write(@pattern_file2, '$pattern2_loaded = true')
87
+
88
+ $pattern1_loaded = false
89
+ $pattern2_loaded = false
90
+
91
+ Familia::Autoloader.autoload_files([
92
+ File.join(@pattern_dir1, '*.rb'),
93
+ File.join(@pattern_dir2, '*.rb')
94
+ ])
95
+
96
+ $pattern1_loaded && $pattern2_loaded
97
+ #=> true
98
+
99
+ ## Test that included method loads features from features directory
100
+ # Create a mock module that includes Autoloader
101
+ @mock_features_module = Module.new do
102
+ include Familia::Autoloader
103
+ end
104
+
105
+ # The Features module already includes Autoloader, so test indirectly
106
+ Familia::Features.ancestors.include?(Familia::Autoloader)
107
+ #=> true
108
+
109
+ # Cleanup test files and directories
110
+ FileUtils.rm_rf(@test_dir)
111
+ FileUtils.rm_rf(@exclude_test_dir)
112
+ FileUtils.rm_rf(@pattern_test_dir)
@@ -1,59 +1,76 @@
1
1
  require_relative '../helpers/test_helpers'
2
2
 
3
+ module RefinedContext
4
+ using Familia::Refinements::TimeUtils
5
+
6
+ # This helper evaluates code within the refined context using eval.
7
+ # This works because eval executes the code as if it were written
8
+ # at this location, making the refinements available.
9
+ def self.eval_in_refined_context(code)
10
+ eval(code)
11
+ end
12
+
13
+ # This helper also evaluates code in the refined context using instance_eval.
14
+ # This provides an alternative approach for testing refinements.
15
+ def self.instance_eval_in_refined_context(code)
16
+ instance_eval(code)
17
+ end
18
+ end
19
+
3
20
  # Test core extensions
4
21
 
5
22
  ## String time parsing - seconds
6
- '60s'.in_seconds
7
- #=> 60
23
+ RefinedContext.eval_in_refined_context("'60s'.in_seconds")
24
+ #=> 60.0
8
25
 
9
26
  ## String time parsing - minutes
10
- '5m'.in_seconds
11
- #=> 300
27
+ RefinedContext.instance_eval_in_refined_context("'5m'.in_seconds")
28
+ #=> 300.0
12
29
 
13
30
  ## String time parsing - hours
14
- '2h'.in_seconds
15
- #=> 7200
31
+ RefinedContext.eval_in_refined_context("'2h'.in_seconds")
32
+ #=> 7200.0
16
33
 
17
34
  ## String time parsing - days
18
- '1d'.in_seconds
19
- #=> 86_400
35
+ RefinedContext.instance_eval_in_refined_context("'1d'.in_seconds")
36
+ #=> 86_400.0
20
37
 
21
- ## String time parsing - days
22
- '1y'.in_seconds
23
- #=> 31536000
38
+ ## String time parsing - years
39
+ RefinedContext.eval_in_refined_context("'1y'.in_seconds")
40
+ #=> 31556952.0
24
41
 
25
42
  ## Time::Units - second
26
- 1.second
43
+ RefinedContext.instance_eval_in_refined_context("1.second")
27
44
  #=> 1
28
45
 
29
46
  ## Time::Units - minute
30
- 1.minute
47
+ RefinedContext.eval_in_refined_context("1.minute")
31
48
  #=> 60
32
49
 
33
50
  ## Time::Units - hour
34
- 1.hour
51
+ RefinedContext.instance_eval_in_refined_context("1.hour")
35
52
  #=> 3600
36
53
 
37
54
  ## Time::Units - day
38
- 1.day
55
+ RefinedContext.eval_in_refined_context("1.day")
39
56
  #=> 86_400
40
57
 
41
58
  ## Time::Units - week
42
- 1.week
59
+ RefinedContext.instance_eval_in_refined_context("1.week")
43
60
  #=> 604_800
44
61
 
45
62
  ## Numeric extension to_ms
46
- 1000.to_ms
47
- #=> 1000 * 1000
63
+ RefinedContext.eval_in_refined_context("1000.to_ms")
64
+ #=> 1000000.0
48
65
 
49
66
  ## Numeric extension to_bytes - single byte
50
- 1.to_bytes
67
+ RefinedContext.instance_eval_in_refined_context("1.to_bytes")
51
68
  #=> '1.00 B'
52
69
 
53
70
  ## Numeric extension to_bytes - kilobytes
54
- 1024.to_bytes
71
+ RefinedContext.eval_in_refined_context("1024.to_bytes")
55
72
  #=> '1.00 KiB'
56
73
 
57
74
  ## Numeric extension to_bytes - megabytes
58
- (1024 * 1024).to_bytes
75
+ RefinedContext.instance_eval_in_refined_context("(1024 * 1024).to_bytes")
59
76
  #=> '1.00 MiB'
@@ -44,14 +44,15 @@ parsed_time = Familia.now(Time.parse('2011-04-10 20:56:20 UTC').utc)
44
44
  #=> [1302468980.0, true, true]
45
45
 
46
46
  ## Familia.qnow
47
- Familia.qstamp 10.minutes, time: 1_302_468_980
47
+ RefinedContext.eval_in_refined_context("Familia.qstamp 10.minutes, time: 1_302_468_980")
48
48
  #=> 1302468600
49
49
 
50
50
  ## Familia::Object.qstamp
51
- Limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)
51
+ RefinedContext.eval_in_refined_context("Limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)")
52
52
  #=> '20:50'
53
53
 
54
54
  ## Familia::Object#qstamp
55
55
  limiter = Limiter.new :request
56
- limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)
56
+ RefinedContext.instance_variable_set(:@limiter, limiter)
57
+ RefinedContext.eval_in_refined_context("@limiter.qstamp(10.minutes, pattern: '%H:%M', time: 1_302_468_980)")
57
58
  #=> '20:50'