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
@@ -17,11 +17,11 @@ class User < Familia::Horreum
17
17
  field :user_id
18
18
  field :email
19
19
  field :name
20
- field :role # admin, editor, viewer, guest
20
+ field :role # admin, editor, viewer, guest
21
21
  field :created_at
22
22
 
23
- sorted_set :documents # Documents this user can access
24
- sorted_set :recent_activity # Recent document access
23
+ sorted_set :documents # Documents this user can access
24
+ sorted_set :recent_activity # Recent document access
25
25
  end
26
26
 
27
27
  class Document < Familia::Horreum
@@ -39,10 +39,10 @@ class Document < Familia::Horreum
39
39
  field :content
40
40
  field :created_at
41
41
  field :updated_at
42
- field :document_type # public, private, confidential
42
+ field :document_type # public, private, confidential
43
43
 
44
- sorted_set :collaborators # Users with access to this document
45
- list :audit_log # Track permission changes and access
44
+ sorted_set :collaborators # Users with access to this document
45
+ list :audit_log # Track permission changes and access
46
46
 
47
47
  # Add document to user's collection with specific permissions
48
48
  def share_with_user(user, *permissions)
@@ -78,7 +78,7 @@ class Document < Familia::Horreum
78
78
 
79
79
  # Get users with specific permission level or higher
80
80
  def users_with_permission(*required_permissions)
81
- all_permissions.select do |user_id, user_perms|
81
+ all_permissions.select do |_user_id, user_perms|
82
82
  required_permissions.all? { |perm| user_perms.include?(perm) }
83
83
  end.keys
84
84
  end
@@ -102,7 +102,7 @@ class Document < Familia::Horreum
102
102
  active_users: active_users,
103
103
  total_collaborators: collaborators.size,
104
104
  permission_breakdown: all_permissions,
105
- audit_entries: audit_log.range(0, 50)
105
+ audit_entries: audit_log.range(0, 50),
106
106
  }
107
107
  end
108
108
  end
@@ -113,10 +113,10 @@ class DocumentService
113
113
  ROLE_PERMISSIONS = {
114
114
  guest: [:read],
115
115
  viewer: [:read],
116
- commenter: [:read, :append],
117
- editor: [:read, :write, :edit],
118
- reviewer: [:read, :write, :edit, :delete],
119
- admin: [:read, :write, :edit, :delete, :configure, :transfer, :admin]
116
+ commenter: %i[read append],
117
+ editor: %i[read write edit],
118
+ reviewer: %i[read write edit delete],
119
+ admin: %i[read write edit delete configure transfer admin],
120
120
  }.freeze
121
121
 
122
122
  def self.create_document(owner, title, content, doc_type = 'private')
@@ -164,7 +164,7 @@ class DocumentService
164
164
 
165
165
  documents.each do |doc|
166
166
  users.each do |user|
167
- doc.revoke_access(user) # Clear existing
167
+ doc.revoke_access(user) # Clear existing
168
168
  doc.share_with_user(user, *permissions) if permissions
169
169
  end
170
170
  end
@@ -173,8 +173,8 @@ end
173
173
 
174
174
  # Example Usage and Demonstration
175
175
  if __FILE__ == $0
176
- puts "🚀 Familia Bit Encoding Integration Example"
177
- puts "=" * 50
176
+ puts '🚀 Familia Bit Encoding Integration Example'
177
+ puts '=' * 50
178
178
 
179
179
  # Create users
180
180
  alice = User.new(user_id: 'alice', email: 'alice@company.com', name: 'Alice Smith', role: 'admin')
@@ -182,9 +182,9 @@ if __FILE__ == $0
182
182
  charlie = User.new(user_id: 'charlie', email: 'charlie@company.com', name: 'Charlie Brown', role: 'viewer')
183
183
 
184
184
  # Create documents
185
- doc1 = DocumentService.create_document(alice, "Q4 Financial Report", "Confidential financial data...", 'confidential')
186
- doc2 = DocumentService.create_document(alice, "Team Meeting Notes", "Weekly standup notes...", 'private')
187
- doc3 = DocumentService.create_document(bob, "Project Proposal", "New feature proposal...", 'public')
185
+ doc1 = DocumentService.create_document(alice, 'Q4 Financial Report', 'Confidential financial data...', 'confidential')
186
+ doc2 = DocumentService.create_document(alice, 'Team Meeting Notes', 'Weekly standup notes...', 'private')
187
+ doc3 = DocumentService.create_document(bob, 'Project Proposal', 'New feature proposal...', 'public')
188
188
 
189
189
  # Share documents with different permission levels
190
190
  puts "\n📄 Document Sharing:"
@@ -209,14 +209,14 @@ if __FILE__ == $0
209
209
  analytics = doc1.access_analytics
210
210
  puts "Financial Report - Active Users: #{analytics[:active_users].size}"
211
211
  puts "Total Collaborators: #{analytics[:total_collaborators]}"
212
- puts "Permission Breakdown:"
212
+ puts 'Permission Breakdown:'
213
213
  analytics[:permission_breakdown].each do |user_id, perms|
214
214
  puts " #{user_id}: #{perms.join(', ')}"
215
215
  end
216
216
 
217
217
  # Demonstrate bit encoding efficiency
218
218
  puts "\n⚡ Bit Encoding Efficiency:"
219
- score = Familia::Features::Relationships::ScoreEncoding.encode_score(Time.now, [:read, :write, :edit, :delete])
219
+ score = Familia::Features::Relationships::ScoreEncoding.encode_score(Time.now, %i[read write edit delete])
220
220
  decoded = Familia::Features::Relationships::ScoreEncoding.decode_score(score)
221
221
  puts "Encoded score: #{score}"
222
222
  puts "Decoded permissions: #{decoded[:permission_list].join(', ')}"
@@ -225,13 +225,16 @@ if __FILE__ == $0
225
225
  # Cleanup
226
226
  puts "\n🧹 Cleanup:"
227
227
  [alice, bob, charlie].each { |user| user.documents.clear }
228
- [doc1, doc2, doc3].each { |doc| doc.clear_all_permissions; doc.collaborators.clear }
228
+ [doc1, doc2, doc3].each do |doc|
229
+ doc.clear_all_permissions
230
+ doc.collaborators.clear
231
+ end
229
232
 
230
- puts "✅ Integration example completed successfully!"
233
+ puts '✅ Integration example completed successfully!'
231
234
  puts "\nThis demonstrates:"
232
- puts "• Fine-grained permission management with 8-bit encoding"
233
- puts "• Role-based access control with business logic"
234
- puts "• Time-based analytics and audit trails"
235
- puts "• Efficient Redis storage with sorted sets"
236
- puts "• Production-ready error handling and validation"
235
+ puts '• Fine-grained permission management with 8-bit encoding'
236
+ puts '• Role-based access control with business logic'
237
+ puts '• Time-based analytics and audit trails'
238
+ puts '• Efficient Redis storage with sorted sets'
239
+ puts '• Production-ready error handling and validation'
237
240
  end
@@ -49,7 +49,7 @@ class Domain < Familia::Horreum
49
49
  field :status
50
50
 
51
51
  # Declare membership in customer collections
52
- member_of Customer, :domains #, type: :set
52
+ member_of Customer, :domains # , type: :set
53
53
 
54
54
  # Track domains by status (using class_ prefix for class-level)
55
55
  class_tracked_in :active_domains,
@@ -107,7 +107,7 @@ puts
107
107
  puts '=== 2. Establishing Relationships ==='
108
108
 
109
109
  # Save objects to automatically update indexes and class-level tracking
110
- customer.save # Automatically adds to email_lookup, plan_lookup, and all_customers
110
+ customer.save # Automatically adds to email_lookup, plan_lookup, and all_customers
111
111
  puts '✓ Customer automatically added to indexes and tracking on save'
112
112
 
113
113
  # Establish member_of relationships using clean << operator syntax
@@ -173,7 +173,6 @@ active_domain_scores.each_slice(2) do |domain_id, timestamp|
173
173
  end
174
174
  puts
175
175
 
176
-
177
176
  puts '=== 6. Relationship Cleanup ==='
178
177
 
179
178
  # Remove relationships
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # examples/safe_dump.rb
4
+ #
5
+ # Demonstrates the SafeDump feature with the new DSL methods.
6
+ # SafeDump allows you to control which fields are exposed when
7
+ # serializing objects, preventing accidental exposure of sensitive data.
8
+
9
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
10
+ require 'familia'
11
+
12
+ # Configure connection
13
+ Familia.uri = 'redis://localhost:6379/15'
14
+
15
+ puts '=== SafeDump Feature Examples ==='
16
+ puts
17
+
18
+ # Example 1: Basic SafeDump with simple fields
19
+ class User < Familia::Horreum
20
+ feature :safe_dump
21
+
22
+ identifier_field :email
23
+ field :email
24
+ field :first_name
25
+ field :last_name
26
+ field :password_hash # Sensitive - not included in safe dump
27
+ field :ssn # Sensitive - not included in safe dump
28
+ field :created_at
29
+
30
+ # Define safe dump fields using the new DSL
31
+ safe_dump_field :email
32
+ safe_dump_field :first_name
33
+ safe_dump_field :last_name
34
+ safe_dump_field :created_at
35
+ end
36
+
37
+ puts 'Example 1: Basic SafeDump'
38
+ user = User.new(
39
+ email: 'alice@example.com',
40
+ first_name: 'Alice',
41
+ last_name: 'Smith',
42
+ password_hash: 'secret123',
43
+ ssn: '123-45-6789',
44
+ created_at: Time.now.to_i
45
+ )
46
+
47
+ puts "Full object data: #{user.to_h}"
48
+ puts "Safe dump: #{user.safe_dump}"
49
+ puts 'Notice: password_hash and ssn are excluded'
50
+ puts
51
+
52
+ # Example 2: SafeDump with computed fields using callables
53
+ class Product < Familia::Horreum
54
+ feature :safe_dump
55
+
56
+ identifier_field :sku
57
+ field :sku
58
+ field :name
59
+ field :price_cents # Store price in cents internally
60
+ field :cost_cents # Sensitive - don't expose
61
+ field :inventory_count
62
+ field :category
63
+ field :created_at
64
+
65
+ # Mix simple fields with computed fields
66
+ safe_dump_field :sku
67
+ safe_dump_field :name
68
+ safe_dump_field :category
69
+ safe_dump_field :created_at
70
+
71
+ # Computed fields using callables
72
+ safe_dump_field :price, ->(product) { "$#{format('%.2f', product.price_cents.to_i / 100.0)}" }
73
+ safe_dump_field :in_stock, ->(product) { product.inventory_count.to_i > 0 }
74
+ safe_dump_field :display_name, ->(product) { "#{product.name} (#{product.sku})" }
75
+ end
76
+
77
+ puts 'Example 2: SafeDump with computed fields'
78
+ product = Product.new(
79
+ sku: 'WIDGET-001',
80
+ name: 'Super Widget',
81
+ price_cents: 1599, # $15.99
82
+ cost_cents: 800, # $8.00 - sensitive, not exposed
83
+ inventory_count: 25,
84
+ category: 'widgets',
85
+ created_at: Time.now.to_i
86
+ )
87
+
88
+ puts "Full object data: #{product.to_h}"
89
+ puts "Safe dump: #{product.safe_dump}"
90
+ puts 'Notice: price converted to dollars, in_stock computed, cost_cents hidden'
91
+ puts
92
+
93
+ # Example 3: SafeDump with multiple field definition styles
94
+ class Order < Familia::Horreum
95
+ feature :safe_dump
96
+
97
+ identifier_field :order_id
98
+ field :order_id
99
+ field :customer_email
100
+ field :status
101
+ field :total_cents
102
+ field :payment_method
103
+ field :credit_card_number # Very sensitive!
104
+ field :processing_notes # Internal only
105
+ field :created_at
106
+ field :shipped_at
107
+
108
+ # Mix of individual fields and batch definitions
109
+ safe_dump_field :order_id
110
+ safe_dump_field :customer_email
111
+ safe_dump_field :status
112
+
113
+ # Define multiple fields at once
114
+ safe_dump_fields :created_at, :shipped_at
115
+
116
+ # Computed fields using hash syntax
117
+ safe_dump_fields(
118
+ { total: ->(order) { "$#{format('%.2f', order.total_cents.to_i / 100.0)}" } },
119
+ { payment_type: ->(order) { order.payment_method&.split('_')&.first&.capitalize } }
120
+ )
121
+
122
+ def customer_obscured_email
123
+ email = customer_email.to_s
124
+ return email if email.length < 3
125
+
126
+ local, domain = email.split('@', 2)
127
+ return email unless domain
128
+
129
+ obscured_local = local[0] + ('*' * [local.length - 2, 0].max) + local[-1]
130
+ "#{obscured_local}@#{domain}"
131
+ end
132
+ end
133
+
134
+ puts 'Example 3: Multiple definition styles'
135
+ order = Order.new(
136
+ order_id: 'ORD-2024-001',
137
+ customer_email: 'customer@example.com',
138
+ status: 'shipped',
139
+ total_cents: 2499, # $24.99
140
+ payment_method: 'credit_card',
141
+ credit_card_number: '4111-1111-1111-1111', # Never expose this!
142
+ processing_notes: 'Rush order - expedite shipping',
143
+ created_at: Time.now.to_i - 86_400, # Yesterday
144
+ shipped_at: Time.now.to_i - 3600 # 1 hour ago
145
+ )
146
+
147
+ puts "Full object data: #{order.to_h}"
148
+ puts "Safe dump: #{order.safe_dump}"
149
+ puts 'Notice: credit card and internal notes excluded, computed fields included'
150
+ puts
151
+
152
+ # Example 4: SafeDump with nested objects
153
+ class Address < Familia::Horreum
154
+ feature :safe_dump
155
+
156
+ identifier_field :id
157
+ field :id
158
+ field :street
159
+ field :city
160
+ field :state
161
+ field :zip_code
162
+ field :country
163
+
164
+ # Simple address fields
165
+ safe_dump_fields :street, :city, :state, :zip_code, :country
166
+ end
167
+
168
+ class Customer < Familia::Horreum
169
+ feature :safe_dump
170
+
171
+ identifier_field :id
172
+ field :id
173
+ field :name
174
+ field :email
175
+ field :phone
176
+ field :billing_address_id
177
+ field :shipping_address_id
178
+ field :account_balance_cents
179
+ field :credit_limit_cents # Sensitive
180
+ field :internal_notes # Internal only
181
+
182
+ safe_dump_field :id
183
+ safe_dump_field :name
184
+ safe_dump_field :email
185
+ safe_dump_field :phone
186
+
187
+ # Nested object handling - load and safe_dump related addresses
188
+ safe_dump_field :billing_address, lambda { |customer|
189
+ addr_id = customer.billing_address_id
190
+ addr_id ? Address.load(addr_id)&.safe_dump : nil
191
+ }
192
+
193
+ safe_dump_field :shipping_address, lambda { |customer|
194
+ addr_id = customer.shipping_address_id
195
+ addr_id ? Address.load(addr_id)&.safe_dump : nil
196
+ }
197
+
198
+ safe_dump_field :account_balance, lambda { |customer|
199
+ "$#{format('%.2f', customer.account_balance_cents.to_i / 100.0)}"
200
+ }
201
+ end
202
+
203
+ puts 'Example 4: SafeDump with nested objects'
204
+
205
+ # Create addresses first
206
+ billing = Address.new(
207
+ id: 'addr_1',
208
+ street: '123 Main St',
209
+ city: 'Anytown',
210
+ state: 'CA',
211
+ zip_code: '90210',
212
+ country: 'USA'
213
+ )
214
+ billing.save
215
+
216
+ shipping = Address.new(
217
+ id: 'addr_2',
218
+ street: '456 Oak Ave',
219
+ city: 'Somewhere',
220
+ state: 'NY',
221
+ zip_code: '10001',
222
+ country: 'USA'
223
+ )
224
+ shipping.save
225
+
226
+ customer = Customer.new(
227
+ id: 'cust_123',
228
+ name: 'Bob Johnson',
229
+ email: 'bob@example.com',
230
+ phone: '555-1234',
231
+ billing_address_id: 'addr_1',
232
+ shipping_address_id: 'addr_2',
233
+ account_balance_cents: 15_000, # $150.00
234
+ credit_limit_cents: 100_000, # $1000.00 - sensitive!
235
+ internal_notes: 'VIP customer - handle with care'
236
+ )
237
+
238
+ puts 'Customer safe dump:'
239
+ puts JSON.pretty_generate(customer.safe_dump)
240
+ puts 'Notice: Nested addresses included, sensitive credit limit excluded'
241
+ puts
242
+
243
+ # Example 5: Introspection methods
244
+ puts 'Example 5: SafeDump introspection'
245
+ puts "User safe dump field names: #{User.safe_dump_field_names}"
246
+ puts "Product safe dump field names: #{Product.safe_dump_field_names}"
247
+ puts "Order safe dump field names: #{Order.safe_dump_field_names}"
248
+ puts
249
+
250
+ puts "User safe dump field map keys: #{User.safe_dump_field_map.keys}"
251
+ puts "All Product safe dump fields are callable: #{Product.safe_dump_field_map.values.all? do |v|
252
+ v.respond_to?(:call)
253
+ end}"
254
+ puts
255
+
256
+ # Example 6: Legacy compatibility methods
257
+ puts 'Example 6: Legacy compatibility'
258
+ puts "Using legacy safe_dump_fields getter: #{User.safe_dump_fields}"
259
+ puts 'Setting fields with set_safe_dump_fields:'
260
+
261
+ class LegacyModel < Familia::Horreum
262
+ feature :safe_dump
263
+ identifier_field :id
264
+ field :id
265
+ field :name
266
+ field :value
267
+ end
268
+
269
+ LegacyModel.set_safe_dump_fields(:id, :name)
270
+ puts "LegacyModel fields after set_safe_dump_fields: #{LegacyModel.safe_dump_fields}"
271
+
272
+ # Clean up
273
+ puts
274
+ puts '=== Cleaning up test data ==='
275
+ [User, Product, Order, Address, Customer, LegacyModel].each do |klass|
276
+ klass.redis.del(klass.redis.keys("#{klass.name.downcase}:*"))
277
+ rescue StandardError => e
278
+ puts "Error cleaning #{klass}: #{e.message}"
279
+ end
280
+
281
+ puts 'SafeDump examples completed!'
data/familia.gemspec CHANGED
@@ -19,10 +19,11 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.required_ruby_version = Gem::Requirement.new('>= 3.4')
21
21
 
22
- spec.add_dependency 'benchmark', '~> 0.1'
23
- spec.add_dependency 'connection_pool', '~> 2.4'
24
- spec.add_dependency 'csv', '~> 3.1'
25
- spec.add_dependency 'logger', '~> 1.6'
22
+ spec.add_dependency 'benchmark', '~> 0.4'
23
+ spec.add_dependency 'connection_pool', '~> 2.5'
24
+ spec.add_dependency 'csv', '~> 3.3'
25
+ spec.add_dependency 'logger', '~> 1.7'
26
+ spec.add_dependency 'oj', '~> 3.16'
26
27
  spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
27
28
  spec.add_dependency 'stringio', '~> 3.1.1'
28
29
  spec.add_dependency 'uri-valkey', '~> 1.4'
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ # Provides autoloading functionality for Ruby files based on patterns and conventions.
5
+ #
6
+ # Used by the Features module at library startup to load feature files, and available
7
+ # as a utility for other modules requiring file autoloading capabilities.
8
+ module Autoloader
9
+ # Autoloads Ruby files matching the given patterns.
10
+ #
11
+ # @param patterns [String, Array<String>] file patterns to match (supports Dir.glob patterns)
12
+ # @param exclude [Array<String>] basenames to exclude from loading
13
+ # @param log_prefix [String] prefix for debug logging messages
14
+ def self.autoload_files(patterns, exclude: [], log_prefix: 'Autoloader')
15
+ patterns = Array(patterns)
16
+
17
+ patterns.each do |pattern|
18
+ Dir.glob(pattern).each do |file_path|
19
+ basename = File.basename(file_path)
20
+
21
+ # Skip excluded files
22
+ next if exclude.include?(basename)
23
+
24
+ Familia.trace :FEATURE, nil, "[#{log_prefix}] Loading #{file_path}", caller(1..1) if Familia.debug?
25
+ require File.expand_path(file_path)
26
+ end
27
+ end
28
+ end
29
+
30
+ # Autoloads feature files when this module is included.
31
+ #
32
+ # Discovers and loads all Ruby files in the features/ directory relative to the
33
+ # including module's location. Typically used by Familia::Features.
34
+ #
35
+ # @param base [Module] the module including this autoloader
36
+ def self.included(base)
37
+ # Get the directory where the including module is defined
38
+ # This should be lib/familia for the Features module
39
+ base_path = File.dirname(caller_locations(1, 1).first.path)
40
+ features_dir = File.join(base_path, 'features')
41
+
42
+ Familia.ld "[DEBUG] Autoloader loading features from #{features_dir}"
43
+
44
+ return unless Dir.exist?(features_dir)
45
+
46
+ # Use the shared autoload_files method
47
+ autoload_files(
48
+ File.join(features_dir, '*.rb'),
49
+ log_prefix: 'Autoloader'
50
+ )
51
+ end
52
+ end
53
+ end
data/lib/familia/base.rb CHANGED
@@ -14,11 +14,48 @@ module Familia
14
14
  # @see Familia::DataType
15
15
  #
16
16
  module Base
17
+
18
+ using Familia::Refinements::TimeUtils
19
+
17
20
  @features_available = nil
18
21
  @feature_definitions = nil
19
22
  @dump_method = :to_json
20
23
  @load_method = :from_json
21
24
 
25
+ def self.included(base)
26
+ # Ensure the including class gets its own feature registry
27
+ base.extend(ClassMethods)
28
+ end
29
+
30
+ # Familia::Base::ClassMethods
31
+ #
32
+ module ClassMethods
33
+ attr_reader :features_available, :feature_definitions
34
+ attr_accessor :dump_method, :load_method
35
+
36
+ def add_feature(klass, feature_name, depends_on: [])
37
+ @features_available ||= {}
38
+ Familia.trace :ADD_FEATURE, klass, feature_name, caller(1..1) if Familia.debug?
39
+
40
+ # Create field definition object
41
+ feature_def = FeatureDefinition.new(
42
+ name: feature_name,
43
+ depends_on: depends_on,
44
+ )
45
+
46
+ # Track field definitions after defining field methods
47
+ @feature_definitions ||= {}
48
+ @feature_definitions[feature_name] = feature_def
49
+
50
+ features_available[feature_name] = klass
51
+ end
52
+
53
+ # Find a feature by name, traversing this class's ancestry chain
54
+ def find_feature(feature_name)
55
+ Familia::Base.find_feature(feature_name, self)
56
+ end
57
+ end
58
+
22
59
  # Returns a string representation of the object. Implementing classes
23
60
  # are welcome to override this method to provide a more meaningful
24
61
  # representation. Using this as a default via super is recommended.
@@ -29,6 +66,7 @@ module Familia
29
66
  "#<#{self.class}:0x#{object_id.to_s(16)}>"
30
67
  end
31
68
 
69
+ # Module-level methods for Familia::Base itself
32
70
  class << self
33
71
  attr_reader :features_available, :feature_definitions
34
72
  attr_accessor :dump_method, :load_method
@@ -49,6 +87,25 @@ module Familia
49
87
 
50
88
  features_available[feature_name] = klass
51
89
  end
90
+
91
+ # Find a feature by name, traversing the ancestry chain of classes
92
+ # that include Familia::Base
93
+ def find_feature(feature_name, starting_class = self)
94
+ # Convert to symbol for consistent lookup
95
+ feature_name = feature_name.to_sym
96
+
97
+ # Walk up the ancestry chain, checking each class that includes Familia::Base
98
+ starting_class.ancestors.each do |ancestor|
99
+ next unless ancestor.respond_to?(:features_available)
100
+ next unless ancestor.features_available
101
+
102
+ if ancestor.features_available.key?(feature_name)
103
+ return ancestor.features_available[feature_name]
104
+ end
105
+ end
106
+
107
+ nil
108
+ end
52
109
  end
53
110
 
54
111
  def generate_id
@@ -3,6 +3,8 @@
3
3
  require_relative 'data_type/commands'
4
4
  require_relative 'data_type/serialization'
5
5
 
6
+ # Familia
7
+ #
6
8
  module Familia
7
9
  # DataType - Base class for Database data type wrappers
8
10
  #
@@ -14,6 +16,8 @@ module Familia
14
16
  include Familia::Base
15
17
  extend Familia::Features
16
18
 
19
+ using Familia::Refinements::TimeUtils
20
+
17
21
  @registered_types = {}
18
22
  @valid_options = %i[class parent default_expiration default logical_database dbkey dbclient suffix prefix]
19
23
  @logical_database = nil
@@ -9,7 +9,7 @@ module Familia
9
9
  return false unless json_string.kind_of?(::String)
10
10
 
11
11
  begin
12
- parsed = JSON.parse(json_string, symbolize_names: true)
12
+ parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
13
13
  return false unless parsed.is_a?(Hash)
14
14
 
15
15
  # Check for required fields
@@ -17,7 +17,7 @@ module Familia
17
17
  result = required_fields.all? { |field| parsed.key?(field) }
18
18
  Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
19
19
  result
20
- rescue JSON::ParserError => e
20
+ rescue Familia::SerializerError => e
21
21
  Familia.ld "[valid?] JSON error: #{e.message}"
22
22
  false
23
23
  end
@@ -31,8 +31,8 @@ module Familia
31
31
  end
32
32
 
33
33
  begin
34
- parsed = JSON.parse(json_string, symbolize_names: true)
35
- rescue JSON::ParserError => e
34
+ parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
35
+ rescue Familia::SerializerError => e
36
36
  raise EncryptionError, "Invalid JSON structure: #{e.message}"
37
37
  end
38
38
 
@@ -19,13 +19,15 @@ module Familia
19
19
 
20
20
  result = @provider.encrypt(plaintext, key, additional_data)
21
21
 
22
- Familia::Encryption::EncryptedData.new(
22
+ encrypted_data = Familia::Encryption::EncryptedData.new(
23
23
  algorithm: @provider.algorithm,
24
24
  nonce: Base64.strict_encode64(result[:nonce]),
25
25
  ciphertext: Base64.strict_encode64(result[:ciphertext]),
26
26
  auth_tag: Base64.strict_encode64(result[:auth_tag]),
27
27
  key_version: current_key_version
28
- ).to_h.to_json
28
+ ).to_h
29
+
30
+ Familia::JsonSerializer.dump(encrypted_data)
29
31
  ensure
30
32
  Familia::Encryption.secure_wipe(key) if key
31
33
  end
@@ -37,7 +39,7 @@ module Familia
37
39
  Familia::Encryption.derivation_count.increment
38
40
 
39
41
  begin
40
- data = Familia::Encryption::EncryptedData.new(**JSON.parse(encrypted_json, symbolize_names: true))
42
+ data = Familia::Encryption::EncryptedData.new(**Familia::JsonSerializer.parse(encrypted_json, symbolize_names: true))
41
43
 
42
44
  # Validate algorithm support
43
45
  provider = Registry.get(data.algorithm)
@@ -51,7 +53,7 @@ module Familia
51
53
  provider.decrypt(ciphertext, key, nonce, auth_tag, additional_data)
52
54
  rescue EncryptionError
53
55
  raise
54
- rescue JSON::ParserError => e
56
+ rescue Familia::SerializerError => e
55
57
  raise EncryptionError, "Invalid JSON structure: #{e.message}"
56
58
  rescue StandardError => e
57
59
  raise EncryptionError, "Decryption failed: #{e.message}"
@@ -1,4 +1,4 @@
1
- # lib/familia/encryption_request_cache.rb
1
+ # lib/familia/encryption/request_cache.rb
2
2
  #
3
3
  # Request-scoped caching for encryption keys (if needed for performance)
4
4
  # This should ONLY be enabled if performance testing shows it's necessary