familia 2.0.0.pre10 → 2.0.0.pre12

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -12
  3. data/CLAUDE.md +4 -54
  4. data/Gemfile.lock +1 -1
  5. data/changelog.d/README.md +45 -34
  6. data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
  7. data/docs/archive/FAMILIA_UPDATE.md +1 -1
  8. data/docs/archive/README.md +15 -19
  9. data/docs/guides/Home.md +1 -1
  10. data/docs/guides/Implementation-Guide.md +1 -1
  11. data/docs/guides/relationships-methods.md +1 -1
  12. data/docs/migrating/.gitignore +2 -0
  13. data/docs/migrating/v2.0.0-pre.md +84 -0
  14. data/docs/migrating/v2.0.0-pre11.md +255 -0
  15. data/docs/migrating/v2.0.0-pre12.md +306 -0
  16. data/docs/migrating/v2.0.0-pre5.md +110 -0
  17. data/docs/migrating/v2.0.0-pre6.md +154 -0
  18. data/docs/migrating/v2.0.0-pre7.md +222 -0
  19. data/docs/overview.md +6 -7
  20. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  21. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  22. data/examples/{relationships_basic.rb → relationships.rb} +2 -3
  23. data/examples/safe_dump.rb +281 -0
  24. data/familia.gemspec +4 -4
  25. data/lib/familia/base.rb +52 -0
  26. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  27. data/lib/familia/errors.rb +2 -0
  28. data/lib/familia/features/autoloader.rb +57 -0
  29. data/lib/familia/features/external_identifier.rb +310 -0
  30. data/lib/familia/features/object_identifier.rb +307 -0
  31. data/lib/familia/features/safe_dump.rb +66 -72
  32. data/lib/familia/features.rb +93 -5
  33. data/lib/familia/horreum/subclass/definition.rb +47 -3
  34. data/lib/familia/secure_identifier.rb +51 -75
  35. data/lib/familia/verifiable_identifier.rb +162 -0
  36. data/lib/familia/version.rb +1 -1
  37. data/lib/familia.rb +1 -0
  38. data/setup.cfg +1 -8
  39. data/try/core/secure_identifier_try.rb +47 -18
  40. data/try/core/verifiable_identifier_try.rb +171 -0
  41. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  42. data/try/features/feature_improvements_try.rb +126 -0
  43. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  44. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  45. data/try/features/real_feature_integration_try.rb +7 -6
  46. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  47. data/try/helpers/test_helpers.rb +17 -17
  48. metadata +30 -22
  49. data/changelog.d/fragments/.keep +0 -0
  50. data/changelog.d/template.md.j2 +0 -29
  51. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  52. data/lib/familia/features/external_identifiers.rb +0 -111
  53. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  54. data/lib/familia/features/object_identifiers.rb +0 -194
@@ -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,10 @@ 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
26
  spec.add_dependency 'redis', '>= 4.8.1', '< 6.0'
27
27
  spec.add_dependency 'stringio', '~> 3.1.1'
28
28
  spec.add_dependency 'uri-valkey', '~> 1.4'
data/lib/familia/base.rb CHANGED
@@ -19,6 +19,38 @@ module Familia
19
19
  @dump_method = :to_json
20
20
  @load_method = :from_json
21
21
 
22
+ def self.included(base)
23
+ # Ensure the including class gets its own feature registry
24
+ base.extend(ClassMethods)
25
+ end
26
+
27
+ module ClassMethods
28
+ attr_reader :features_available, :feature_definitions
29
+ attr_accessor :dump_method, :load_method
30
+
31
+ def add_feature(klass, feature_name, depends_on: [])
32
+ @features_available ||= {}
33
+ Familia.trace :ADD_FEATURE, klass, feature_name, caller(1..1) if Familia.debug?
34
+
35
+ # Create field definition object
36
+ feature_def = FeatureDefinition.new(
37
+ name: feature_name,
38
+ depends_on: depends_on,
39
+ )
40
+
41
+ # Track field definitions after defining field methods
42
+ @feature_definitions ||= {}
43
+ @feature_definitions[feature_name] = feature_def
44
+
45
+ features_available[feature_name] = klass
46
+ end
47
+
48
+ # Find a feature by name, traversing this class's ancestry chain
49
+ def find_feature(feature_name)
50
+ Familia::Base.find_feature(feature_name, self)
51
+ end
52
+ end
53
+
22
54
  # Returns a string representation of the object. Implementing classes
23
55
  # are welcome to override this method to provide a more meaningful
24
56
  # representation. Using this as a default via super is recommended.
@@ -29,6 +61,7 @@ module Familia
29
61
  "#<#{self.class}:0x#{object_id.to_s(16)}>"
30
62
  end
31
63
 
64
+ # Module-level methods for Familia::Base itself
32
65
  class << self
33
66
  attr_reader :features_available, :feature_definitions
34
67
  attr_accessor :dump_method, :load_method
@@ -49,6 +82,25 @@ module Familia
49
82
 
50
83
  features_available[feature_name] = klass
51
84
  end
85
+
86
+ # Find a feature by name, traversing the ancestry chain of classes
87
+ # that include Familia::Base
88
+ def find_feature(feature_name, starting_class = self)
89
+ # Convert to symbol for consistent lookup
90
+ feature_name = feature_name.to_sym
91
+
92
+ # Walk up the ancestry chain, checking each class that includes Familia::Base
93
+ starting_class.ancestors.each do |ancestor|
94
+ next unless ancestor.respond_to?(:features_available)
95
+ next unless ancestor.features_available
96
+
97
+ if ancestor.features_available.key?(feature_name)
98
+ return ancestor.features_available[feature_name]
99
+ end
100
+ end
101
+
102
+ nil
103
+ end
52
104
  end
53
105
 
54
106
  def generate_id
@@ -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
@@ -5,6 +5,8 @@ module Familia
5
5
  class NoIdentifier < Problem; end
6
6
  class NonUniqueKey < Problem; end
7
7
 
8
+ class FieldTypeError < Problem; end
9
+
8
10
  class HighRiskFactor < Problem
9
11
  attr_reader :value
10
12
 
@@ -0,0 +1,57 @@
1
+ # lib/familia/features/autoloader.rb
2
+
3
+ module Familia
4
+ module Features
5
+ # Autoloader is a mixin that automatically loads feature files from a 'features'
6
+ # subdirectory when included. This provides a standardized way to organize and
7
+ # auto-load project-specific features.
8
+ #
9
+ # When included in a module, it automatically:
10
+ # 1. Determines the directory containing the module file
11
+ # 2. Looks for a 'features' subdirectory in that location
12
+ # 3. Loads all *.rb files from that features directory
13
+ #
14
+ # Example usage:
15
+ #
16
+ # # apps/api/v2/models/customer/features.rb
17
+ # module V2
18
+ # class Customer < Familia::Horreum
19
+ # module Features
20
+ # include Familia::Features::Autoloader
21
+ # # Automatically loads all files from customer/features/
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # This would automatically load:
27
+ # - apps/api/v2/models/customer/features/deprecated_fields.rb
28
+ # - apps/api/v2/models/customer/features/legacy_support.rb
29
+ # - etc.
30
+ #
31
+ module Autoloader
32
+ def self.included(_base)
33
+ # Get the file path of the module that's including us.
34
+ # `caller_locations(1, 1).first` gives us the location where `include` was called.
35
+ # This is a robust way to find the file path, especially for anonymous modules.
36
+ calling_location = caller_locations(1, 1)&.first
37
+ return unless calling_location
38
+
39
+ including_file = calling_location.path
40
+
41
+ # Find the features directory relative to the including file
42
+ features_dir = File.join(File.dirname(including_file), 'features')
43
+
44
+ Familia.ld "[DEBUG] Autoloader: Looking for features in #{features_dir}"
45
+
46
+ if Dir.exist?(features_dir)
47
+ Dir.glob(File.join(features_dir, '*.rb')).each do |feature_file|
48
+ Familia.ld "[DEBUG] Autoloader: Loading feature #{feature_file}"
49
+ require feature_file
50
+ end
51
+ else
52
+ Familia.ld "[DEBUG] Autoloader: No features directory found at #{features_dir}"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end