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
@@ -56,22 +56,6 @@ hex_trace_id = Familia.generate_hex_trace_id
56
56
  [hex_trace_id.class, hex_trace_id.length == 16, hex_trace_id.match?(/^[a-f0-9]+$/)]
57
57
  #=> [String, true, true]
58
58
 
59
- ## Familia.shorten_to_external_id
60
- Familia.respond_to?(:shorten_to_external_id)
61
- #=> true
62
-
63
- ## Can shorten hex ID to external ID (128 bits)
64
- hex_id = Familia.generate_hex_id
65
- external_id = Familia.shorten_to_external_id(hex_id)
66
- [external_id.class, external_id.length < hex_id.length]
67
- #=> [String, true]
68
-
69
- ## Can shorten hex ID to external ID with custom base (hex)
70
- hex_id = Familia.generate_hex_id
71
- hex_external_id = Familia.shorten_to_external_id(hex_id, base: 16)
72
- [hex_external_id.class, hex_external_id.length == 32]
73
- #=> [String, true]
74
-
75
59
  ## Familia.shorten_to_trace_id
76
60
  Familia.respond_to?(:shorten_to_trace_id)
77
61
  #=> true
@@ -88,10 +72,55 @@ hex_trace_id = Familia.shorten_to_trace_id(hex_id, base: 16)
88
72
  [hex_trace_id.class, hex_trace_id.length == 16]
89
73
  #=> [String, true]
90
74
 
75
+ ## Familia.truncate_hex
76
+ Familia.respond_to?(:truncate_hex)
77
+ #=> true
78
+
79
+ ## Can truncate hex ID to 128 bits by default
80
+ hex_id = Familia.generate_hex_id
81
+ truncated_id = Familia.truncate_hex(hex_id)
82
+ [truncated_id.class, truncated_id.length < hex_id.length]
83
+ #=> [String, true]
84
+
85
+ ## Can truncate hex ID to a custom bit length (64 bits)
86
+ hex_id = Familia.generate_hex_id
87
+ truncated_64 = Familia.truncate_hex(hex_id, bits: 64)
88
+ [truncated_64.class, truncated_64.length < hex_id.length]
89
+ #=> [String, true]
90
+
91
+ ## Can truncate with a custom base (hex)
92
+ hex_id = Familia.generate_hex_id
93
+ hex_truncated = Familia.truncate_hex(hex_id, bits: 128, base: 16)
94
+ [hex_truncated.class, hex_truncated.length == 32]
95
+ #=> [String, true]
96
+
97
+ ## Truncated IDs are deterministic
98
+ hex_id = Familia.generate_hex_id
99
+ id1 = Familia.truncate_hex(hex_id)
100
+ id2 = Familia.truncate_hex(hex_id)
101
+ id1 == id2
102
+ #=> true
103
+
104
+ ## Raises error for invalid hex
105
+ begin
106
+ Familia.truncate_hex("not-a-hex-string")
107
+ rescue ArgumentError => e
108
+ e.message
109
+ end
110
+ #=> "Invalid hexadecimal string: not-a-hex-string"
111
+
112
+ ## Raises error if input bits are less than output bits
113
+ begin
114
+ Familia.truncate_hex("abc", bits: 64)
115
+ rescue ArgumentError => e
116
+ e.message
117
+ end
118
+ #=> "Input bits (12) cannot be less than desired output bits (64)."
119
+
91
120
  ## Shortened IDs are deterministic
92
121
  hex_id = Familia.generate_hex_id
93
- id1 = Familia.shorten_to_external_id(hex_id)
94
- id2 = Familia.shorten_to_external_id(hex_id)
122
+ id1 = Familia.shorten_to_trace_id(hex_id)
123
+ id2 = Familia.shorten_to_trace_id(hex_id)
95
124
  id1 == id2
96
125
  #=> true
97
126
 
@@ -0,0 +1,130 @@
1
+ require_relative '../helpers/test_helpers'
2
+
3
+ module RefinedContext
4
+ using Familia::Refinements::TimeUtils
5
+
6
+ def self.eval_in_refined_context(code)
7
+ eval(code)
8
+ end
9
+
10
+ def self.instance_eval_in_refined_context(code)
11
+ instance_eval(code)
12
+ end
13
+ end
14
+
15
+ # Test TimeUtils refinement
16
+
17
+ ## Numeric#months - convert number to months in seconds
18
+ result = RefinedContext.eval_in_refined_context("1.month")
19
+ result.round(0)
20
+ #=> 2629746.0
21
+
22
+ ## Numeric#months - plural form
23
+ result = RefinedContext.instance_eval_in_refined_context("2.months")
24
+ result.round(0)
25
+ #=> 5259492.0
26
+
27
+ ## Numeric#years - convert number to years in seconds
28
+ result = RefinedContext.eval_in_refined_context("1.year")
29
+ result.round(0)
30
+ #=> 31556952.0
31
+
32
+ ## Numeric#in_months - convert seconds to months
33
+ RefinedContext.instance_eval_in_refined_context("2629746.in_months")
34
+ #=> 1.0
35
+
36
+ ## Numeric#in_years - convert seconds to years
37
+ result = RefinedContext.eval_in_refined_context("#{Familia::Refinements::TimeUtils::PER_YEAR}.in_years")
38
+ result.round(1)
39
+ #=> 1.0
40
+
41
+ ## String#in_seconds - parse month string
42
+ RefinedContext.instance_eval_in_refined_context("'1mo'.in_seconds")
43
+ #=> 2629746.0
44
+
45
+ ## String#in_seconds - parse month string (long form)
46
+ RefinedContext.eval_in_refined_context("'2months'.in_seconds")
47
+ #=> 5259492.0
48
+
49
+ ## String#in_seconds - parse year string
50
+ result = RefinedContext.instance_eval_in_refined_context("'1y'.in_seconds")
51
+ result.round(0)
52
+ #=> 31556952.0
53
+
54
+ ## Numeric#age_in - calculate age in months from timestamp (approximately 1 month ago)
55
+ timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
56
+ result = RefinedContext.eval_in_refined_context("#{timestamp}.age_in(:months)")
57
+ (result - 1.0).abs < 0.01
58
+ #=> true
59
+
60
+ ## Numeric#age_in - calculate age in years from timestamp (approximately 1 year ago)
61
+ timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
62
+ result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.age_in(:years)")
63
+ (result - 1.0).abs < 0.01
64
+ #=> true
65
+
66
+ ## Numeric#months_old - convenience method for age_in(:months)
67
+ timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
68
+ result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
69
+ (result - 1.0).abs < 0.01
70
+ #=> true
71
+
72
+ ## Numeric#years_old - convenience method for age_in(:years)
73
+ timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
74
+ result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
75
+ (result - 1.0).abs < 0.01
76
+ #=> true
77
+
78
+ ## Numeric#months_old - should NOT return seconds (the original bug)
79
+ timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_MONTH
80
+ result = RefinedContext.eval_in_refined_context("#{timestamp}.months_old")
81
+ result.between?(0.9, 1.1) # Should be ~1 month, not millions of seconds
82
+ #=> true
83
+
84
+ ## Numeric#years_old - should NOT return seconds (the original bug)
85
+ timestamp = Time.now.to_f - Familia::Refinements::TimeUtils::PER_YEAR
86
+ result = RefinedContext.instance_eval_in_refined_context("#{timestamp}.years_old")
87
+ result.between?(0.9, 1.1) # Should be ~1 year, not millions of seconds
88
+ #=> true
89
+
90
+ ## age_in with from_time parameter - months
91
+ past_time = Time.now - (2 * Familia::Refinements::TimeUtils::PER_MONTH) # 2 months ago
92
+ from_time = Time.now - Familia::Refinements::TimeUtils::PER_MONTH # 1 month ago
93
+ result = RefinedContext.eval_in_refined_context("#{past_time.to_f}.age_in(:months, #{from_time.to_f})")
94
+ (result - 1.0).abs < 0.01
95
+ #=> true
96
+
97
+ ## age_in with from_time parameter - years
98
+ past_time = Time.now - (2 * Familia::Refinements::TimeUtils::PER_YEAR) # 2 years ago
99
+ from_time = Time.now - Familia::Refinements::TimeUtils::PER_YEAR # 1 year ago
100
+ result = RefinedContext.instance_eval_in_refined_context("#{past_time.to_f}.age_in(:years, #{from_time.to_f})")
101
+ (result - 1.0).abs < 0.01
102
+ #=> true
103
+
104
+ ## Verify month constant is approximately correct (30.437 days)
105
+ expected_seconds_per_month = 30.437 * 24 * 60 * 60
106
+ Familia::Refinements::TimeUtils::PER_MONTH.round(0)
107
+ #=> 2629746.0
108
+
109
+ ## Verify year constant (365.2425 days - Gregorian year)
110
+ expected_seconds_per_year = 365.2425 * 24 * 60 * 60
111
+ Familia::Refinements::TimeUtils::PER_YEAR.round(0)
112
+ #=> 31556952.0
113
+
114
+ ## UNIT_METHODS contains months mapping
115
+ Familia::Refinements::TimeUtils::UNIT_METHODS['months']
116
+ #=> :months
117
+
118
+ ## UNIT_METHODS contains mo mapping
119
+ Familia::Refinements::TimeUtils::UNIT_METHODS['mo']
120
+ #=> :months
121
+
122
+ ## UNIT_METHODS contains month mapping
123
+ Familia::Refinements::TimeUtils::UNIT_METHODS['month']
124
+ #=> :months
125
+
126
+ ## Calendar consistency - 12 months equals 1 year (fix for inconsistency issue)
127
+ result1 = RefinedContext.eval_in_refined_context("12.months")
128
+ result2 = RefinedContext.instance_eval_in_refined_context("1.year")
129
+ result1 == result2
130
+ #=> true
@@ -0,0 +1,171 @@
1
+ # try/core/verifiable_identifier_try.rb
2
+
3
+ require_relative '../helpers/test_helpers'
4
+ require 'familia/verifiable_identifier'
5
+
6
+ ## Module is available
7
+ defined?(Familia::VerifiableIdentifier)
8
+ #=> "constant"
9
+
10
+ ## Uses the development secret key by default when ENV is not set
11
+ Familia::VerifiableIdentifier::SECRET_KEY
12
+ #=> "cafef00dcafef00dcafef00dcafef00dcafef00dcafef00d"
13
+
14
+ # --- Verifiable ID Generation and Verification ---
15
+
16
+ ## Generates a non-empty string ID
17
+ id = Familia::VerifiableIdentifier.generate_verifiable_id
18
+ id.is_a?(String) && !id.empty?
19
+ #=> true
20
+
21
+ ## Generated ID is URL-safe (base36)
22
+ id = Familia::VerifiableIdentifier.generate_verifiable_id
23
+ id.match?(/[a-z0-9]+/ix)
24
+ #=> true
25
+
26
+ ## Generates unique identifiers on subsequent calls
27
+ Familia::VerifiableIdentifier.generate_verifiable_id
28
+ #=/> Familia::VerifiableIdentifier.generate_verifiable_id
29
+
30
+ ## A genuinely generated ID successfully verifies
31
+ id = Familia::VerifiableIdentifier.generate_verifiable_id
32
+ Familia::VerifiableIdentifier.verified_identifier?(id)
33
+ #=> true
34
+
35
+ ## Fails verification for a completely random garbage string
36
+ Familia::VerifiableIdentifier.verified_identifier?('this-is-not-a-valid-id-at-all')
37
+ #=> false
38
+
39
+ ## Fails verification for a string with invalid characters for the base
40
+ # A plus sign is not a valid base-36 character.
41
+ Familia::VerifiableIdentifier.verified_identifier?('this+is+invalid', 36)
42
+ #=> false
43
+
44
+ ## Fails verification if the random part of the ID is tampered with
45
+ id = Familia::VerifiableIdentifier.generate_verifiable_id
46
+ tampered_id = id.dup
47
+ tampered_id[0] = (tampered_id[0] == 'a' ? 'b' : 'a') # Flip the first character
48
+ Familia::VerifiableIdentifier.verified_identifier?(tampered_id)
49
+ #=> false
50
+
51
+ ## Fails verification if the tag part of the ID is tampered with
52
+ id = Familia::VerifiableIdentifier.generate_verifiable_id
53
+ tampered_id = id.dup
54
+ idx = tampered_id.length - 1
55
+ tampered_id[idx] = (tampered_id[idx] == 'a' ? 'b' : 'a') # Flip the last character
56
+ Familia::VerifiableIdentifier.verified_identifier?(tampered_id)
57
+ #=> false
58
+
59
+ ## Works correctly with a different base (hexadecimal)
60
+ id_hex = Familia::VerifiableIdentifier.generate_verifiable_id(16)
61
+ Familia::VerifiableIdentifier.verified_identifier?(id_hex, 16)
62
+ #=> true
63
+
64
+ ## Base 16 ID has the correct hex length (64 random + 16 tag = 80 chars)
65
+ id_hex = Familia::VerifiableIdentifier.generate_verifiable_id(16)
66
+ id_hex.length
67
+ #=> 80
68
+
69
+ # --- Plausibility Checks ---
70
+
71
+ ## A genuinely generated ID is plausible
72
+ id = Familia::VerifiableIdentifier.generate_verifiable_id
73
+ Familia::VerifiableIdentifier.plausible_identifier?(id)
74
+ #=> true
75
+
76
+ ## A well-formed but fake ID is still plausible
77
+ # A string of the correct length (62 for base 36) and charset is plausible
78
+ total_bits = (Familia::VerifiableIdentifier::RANDOM_HEX_LENGTH + Familia::VerifiableIdentifier::TAG_HEX_LENGTH) * 4
79
+ fake_id = 'a' * Familia::SecureIdentifier.min_length_for_bits(total_bits, 36)
80
+ Familia::VerifiableIdentifier.plausible_identifier?(fake_id)
81
+ #=> true
82
+
83
+ ## Fails plausibility check if too short
84
+ short_id = 'a' * 60
85
+ Familia::VerifiableIdentifier.plausible_identifier?(short_id)
86
+ #=> false
87
+
88
+ ## Fails plausibility check if too long
89
+ long_id = 'a' * 66
90
+ Familia::VerifiableIdentifier.plausible_identifier?(long_id)
91
+ #=> false
92
+
93
+ ## Fails plausibility check for invalid characters
94
+ invalid_char_id = 'a' * 61 + '+'
95
+ Familia::VerifiableIdentifier.plausible_identifier?(invalid_char_id)
96
+ #=> false
97
+
98
+ ## Fails plausibility check for nil input
99
+ Familia::VerifiableIdentifier.plausible_identifier?(nil)
100
+ #=> false
101
+
102
+ # --- Scoped Identifier Generation and Verification ---
103
+
104
+ ## Scoped identifier generation produces different results than unscoped
105
+ scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
106
+ unscoped_id = Familia::VerifiableIdentifier.generate_verifiable_id
107
+ scoped_id != unscoped_id
108
+ #=> true
109
+
110
+ ## Scoped identifiers verify successfully with correct scope
111
+ scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
112
+ Familia::VerifiableIdentifier.verified_identifier?(scoped_id, scope: "example.com")
113
+ #=> true
114
+
115
+ ## Scoped identifiers fail verification with wrong scope
116
+ scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
117
+ Familia::VerifiableIdentifier.verified_identifier?(scoped_id, scope: "different.com")
118
+ #=> false
119
+
120
+ ## Scoped identifiers fail verification without scope parameter
121
+ scoped_id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "example.com")
122
+ Familia::VerifiableIdentifier.verified_identifier?(scoped_id)
123
+ #=> false
124
+
125
+ ## Unscoped identifiers fail verification with scope parameter
126
+ unscoped_id = Familia::VerifiableIdentifier.generate_verifiable_id
127
+ Familia::VerifiableIdentifier.verified_identifier?(unscoped_id, scope: "example.com")
128
+ #=> false
129
+
130
+ ## Empty string scope produces different identifier than nil scope
131
+ id_nil = Familia::VerifiableIdentifier.generate_verifiable_id(scope: nil)
132
+ id_empty = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "")
133
+ id_nil != id_empty
134
+ #=> true
135
+
136
+ ## Empty string scope verifies correctly
137
+ id_empty = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "")
138
+ Familia::VerifiableIdentifier.verified_identifier?(id_empty, scope: "")
139
+ #=> true
140
+
141
+ ## Short scope values work correctly
142
+ id_short = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "a")
143
+ Familia::VerifiableIdentifier.verified_identifier?(id_short, scope: "a")
144
+ #=> true
145
+
146
+ ## Long scope values work correctly
147
+ long_scope = "x" * 1000
148
+ id_long = Familia::VerifiableIdentifier.generate_verifiable_id(scope: long_scope)
149
+ Familia::VerifiableIdentifier.verified_identifier?(id_long, scope: long_scope)
150
+ #=> true
151
+
152
+ ## Unicode scope values work correctly
153
+ unicode_scope = "测试🔒🔑"
154
+ id_unicode = Familia::VerifiableIdentifier.generate_verifiable_id(scope: unicode_scope)
155
+ Familia::VerifiableIdentifier.verified_identifier?(id_unicode, scope: unicode_scope)
156
+ #=> true
157
+
158
+ ## Scoped identifiers work with different bases
159
+ id_hex = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "test", base: 16)
160
+ Familia::VerifiableIdentifier.verified_identifier?(id_hex, scope: "test", base: 16)
161
+ #=> true
162
+
163
+ ## Backward compatibility: existing method signatures still work
164
+ id = Familia::VerifiableIdentifier.generate_verifiable_id(36)
165
+ Familia::VerifiableIdentifier.verified_identifier?(id, 36)
166
+ #=> true
167
+
168
+ ## Mixed parameter styles work correctly
169
+ id = Familia::VerifiableIdentifier.generate_verifiable_id(scope: "test", base: 16)
170
+ Familia::VerifiableIdentifier.verified_identifier?(id, scope: "test", base: 16)
171
+ #=> true
@@ -21,7 +21,7 @@ p [@a.name, @b.name]
21
21
  #=> true
22
22
 
23
23
  ## Limiter#qstamp
24
- @limiter1.counter.qstamp(10.minutes, '%H:%M', 1_302_468_980)
24
+ RefinedContext.eval_in_refined_context("@limiter1.counter.qstamp(10.minutes, '%H:%M', 1_302_468_980)")
25
25
  ##=> '20:50'
26
26
 
27
27
  ## Database Types can be stored to quantized stamp suffix
@@ -32,7 +32,8 @@ p [@a.name, @b.name]
32
32
  @limiter2 = Limiter.new :requests
33
33
  p [@limiter1.default_expiration, @limiter2.default_expiration]
34
34
  p [@limiter1.counter.parent.default_expiration, @limiter2.counter.parent.default_expiration]
35
- @limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1_302_468_980)
35
+ RefinedContext.instance_variable_set(:@limiter2, @limiter2)
36
+ RefinedContext.eval_in_refined_context("@limiter2.counter.qstamp(10.minutes, pattern: nil, time: 1_302_468_980)")
36
37
  #=> 1302468600
37
38
 
38
39
  ## Database Types can be stored to quantized numeric suffix. This
@@ -0,0 +1,61 @@
1
+ # try/features/autoloadable/autoloadable_try.rb
2
+
3
+ require_relative '../../../lib/familia'
4
+
5
+ # Create test feature module that includes Autoloadable
6
+ module TestAutoloadableFeature
7
+ include Familia::Features::Autoloadable
8
+
9
+ def self.included(base)
10
+ super
11
+ base.define_method(:test_feature_method) { "feature_loaded" }
12
+ end
13
+ end
14
+
15
+ # Create test class to include the feature
16
+ class TestModelForAutoloadable < Familia::Horreum
17
+ field :name
18
+ end
19
+
20
+ ## Test that Autoloadable can be included in feature modules
21
+ TestAutoloadableFeature.ancestors.include?(Familia::Features::Autoloadable)
22
+ #=> true
23
+
24
+ ## Test that Autoloadable extends feature modules with ClassMethods
25
+ TestAutoloadableFeature.respond_to?(:post_inclusion_autoload)
26
+ #=> true
27
+
28
+ ## Test that including autoloadable feature in Horreum class works
29
+ TestModelForAutoloadable.include(TestAutoloadableFeature)
30
+ TestModelForAutoloadable.ancestors.include?(TestAutoloadableFeature)
31
+ #=> true
32
+
33
+ ## Test that post_inclusion_autoload can be called with test class
34
+ TestAutoloadableFeature.post_inclusion_autoload(TestModelForAutoloadable, :test_autoloadable_feature, {})
35
+ "success"
36
+ #=> "success"
37
+
38
+ ## Test that feature methods are available on the model
39
+ @test_instance = TestModelForAutoloadable.new(name: 'test')
40
+ @test_instance.respond_to?(:test_feature_method)
41
+ #=> true
42
+
43
+ ## Test that feature method works
44
+ @test_instance.test_feature_method
45
+ #=> "feature_loaded"
46
+
47
+ ## Test that Autoloadable works with DataType classes (should not crash)
48
+ class TestDataTypeAutoloadable < Familia::DataType
49
+ include Familia::Features::Autoloadable
50
+ end
51
+
52
+ TestDataTypeAutoloadable.ancestors.include?(Familia::Features::Autoloadable)
53
+ #=> true
54
+
55
+ ## Test that SafeDump includes Autoloadable (real-world usage)
56
+ Familia::Features::SafeDump.ancestors.include?(Familia::Features::Autoloadable)
57
+ #=> true
58
+
59
+ ## Test that SafeDump has post_inclusion_autoload capability
60
+ Familia::Features::SafeDump.respond_to?(:post_inclusion_autoload)
61
+ #=> true
@@ -77,9 +77,14 @@ end
77
77
  @doc.content.to_str
78
78
  #=!> NoMethodError
79
79
 
80
- ## JSON serialization - to_json
81
- @doc.content.to_json
82
- #=> "\"[CONCEALED]\""
80
+ ## JSON serialization - to_json (fails for security)
81
+ begin
82
+ @doc.content.to_json
83
+ raise "Should have raised SerializerError"
84
+ rescue Familia::SerializerError => e
85
+ e.class
86
+ end
87
+ #=> Familia::SerializerError
83
88
 
84
89
  ## JSON serialization - as_json
85
90
  @doc.content.as_json
@@ -188,18 +188,30 @@ rescue TypeError => e
188
188
  end
189
189
  #=> true
190
190
 
191
- ## JSON serialization is safe
192
- @user_json = {
193
- id: @user.id,
194
- username: @user.username,
195
- password: @user.password_hash
196
- }.to_json
197
-
198
- @user_json.include?("bcrypt")
199
- #=> false
191
+ ## JSON serialization prevents leakage by raising error
192
+ begin
193
+ user_json = {
194
+ id: @user.id,
195
+ username: @user.username,
196
+ password: @user.password_hash
197
+ }.to_json
198
+ false
199
+ rescue Familia::SerializerError
200
+ true
201
+ end
202
+ #=> true
200
203
 
201
- ## JSON contains concealed marker
202
- @user_json.include?("[CONCEALED]")
204
+ ## JSON serialization with ConcealedString raises error
205
+ begin
206
+ user_json = {
207
+ id: @user.id,
208
+ username: @user.username,
209
+ password: @user.password_hash
210
+ }.to_json
211
+ false
212
+ rescue Familia::SerializerError => e
213
+ e.message.include?("ConcealedString")
214
+ end
203
215
  #=> true
204
216
 
205
217
  ## Bulk field operations are secure
@@ -281,16 +293,46 @@ api_response = {
281
293
  }
282
294
  }
283
295
 
284
- @response_json = api_response.to_json
285
- @response_json.include?("bcrypt")
286
- #=> false
296
+ begin
297
+ @response_json = api_response.to_json
298
+ false
299
+ rescue Familia::SerializerError
300
+ true
301
+ end
302
+ #=> true
287
303
 
288
304
  ## API response doesn't leak secrets
289
- @response_json.include?("sk-1234567890abcdef")
290
- #=> false
305
+ api_response = {
306
+ user_id: @user.id,
307
+ credentials: {
308
+ password: @user.password_hash,
309
+ api_key: @user.api_secret
310
+ }
311
+ }
312
+
313
+ begin
314
+ @response_json = api_response.to_json
315
+ false
316
+ rescue Familia::SerializerError
317
+ true
318
+ end
319
+ #=> true
291
320
 
292
321
  ## API response contains concealed markers
293
- @response_json.include?("[CONCEALED]")
322
+ api_response = {
323
+ user_id: @user.id,
324
+ credentials: {
325
+ password: @user.password_hash,
326
+ api_key: @user.api_secret
327
+ }
328
+ }
329
+
330
+ begin
331
+ @response_json = api_response.to_json
332
+ false
333
+ rescue Familia::SerializerError => e
334
+ e.message.include?("ConcealedString")
335
+ end
294
336
  #=> true
295
337
 
296
338
  ## Debug logging safety
@@ -67,9 +67,14 @@ hash_result.keys.include?("api_token")
67
67
  @record.api_token.inspect
68
68
  #=> "[CONCEALED]"
69
69
 
70
- ## JSON serialization - to_json
71
- @record.api_token.to_json
72
- #=> "\"[CONCEALED]\""
70
+ ## JSON serialization - to_json (fails for security)
71
+ begin
72
+ @record.api_token.to_json
73
+ raise "Should have raised SerializerError"
74
+ rescue Familia::SerializerError => e
75
+ e.class
76
+ end
77
+ #=> Familia::SerializerError
73
78
 
74
79
  ## JSON serialization - as_json
75
80
  @record.api_token.as_json
@@ -100,12 +105,21 @@ hash_result.keys.include?("api_token")
100
105
  }
101
106
  }
102
107
 
103
- @serialized = @nested_data.to_json
104
- @serialized.include?("token-abc123456789")
105
- #=> false
108
+ begin
109
+ @serialized = @nested_data.to_json
110
+ false
111
+ rescue Familia::SerializerError
112
+ true
113
+ end
114
+ #=> true
106
115
 
107
- ## Nested JSON contains concealed markers
108
- @nested_data.to_json.include?("[CONCEALED]")
116
+ ## Nested JSON with ConcealedString raises error
117
+ begin
118
+ @nested_data.to_json
119
+ false
120
+ rescue Familia::SerializerError => e
121
+ e.message.include?("ConcealedString cannot be serialized")
122
+ end
109
123
  #=> true
110
124
 
111
125
  ## Array of mixed field types safety
@@ -116,11 +130,21 @@ hash_result.keys.include?("api_token")
116
130
  @record.secret_notes
117
131
  ]
118
132
 
119
- @mixed_array.to_json.include?("token-abc123456789")
120
- #=> false
133
+ begin
134
+ @mixed_array.to_json
135
+ false
136
+ rescue Familia::SerializerError
137
+ true
138
+ end
139
+ #=> true
121
140
 
122
- ## Mixed array preserves public data
123
- @mixed_array.to_json.include?("Public Record")
141
+ ## Mixed array with ConcealedString raises error
142
+ begin
143
+ @mixed_array.to_json
144
+ false
145
+ rescue Familia::SerializerError => e
146
+ e.message.include?("ConcealedString")
147
+ end
124
148
  #=> true
125
149
 
126
150
  ## String interpolation safety