familia 2.0.0.pre8 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -0
  3. data/.github/workflows/docs.yml +1 -1
  4. data/.gitignore +9 -9
  5. data/.rubocop.yml +19 -0
  6. data/.yardopts +22 -1
  7. data/CHANGELOG.md +247 -0
  8. data/CLAUDE.md +12 -59
  9. data/Gemfile.lock +1 -1
  10. data/README.md +62 -2
  11. data/changelog.d/README.md +77 -0
  12. data/docs/archive/.gitignore +2 -0
  13. data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
  14. data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
  15. data/docs/archive/FAMILIA_UPDATE.md +226 -0
  16. data/docs/archive/README.md +63 -0
  17. data/docs/guides/.gitignore +2 -0
  18. data/docs/{wiki → guides}/Home.md +1 -1
  19. data/docs/{wiki → guides}/Implementation-Guide.md +1 -1
  20. data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
  21. data/docs/guides/relationships-methods.md +266 -0
  22. data/docs/migrating/.gitignore +2 -0
  23. data/docs/migrating/v2.0.0-pre.md +84 -0
  24. data/docs/migrating/v2.0.0-pre11.md +255 -0
  25. data/docs/migrating/v2.0.0-pre12.md +306 -0
  26. data/docs/migrating/v2.0.0-pre5.md +110 -0
  27. data/docs/migrating/v2.0.0-pre6.md +154 -0
  28. data/docs/migrating/v2.0.0-pre7.md +222 -0
  29. data/docs/overview.md +6 -7
  30. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  31. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  32. data/examples/relationships.rb +205 -0
  33. data/examples/safe_dump.rb +281 -0
  34. data/familia.gemspec +4 -4
  35. data/lib/familia/base.rb +52 -0
  36. data/lib/familia/connection.rb +4 -21
  37. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  38. data/lib/familia/errors.rb +2 -0
  39. data/lib/familia/features/autoloader.rb +57 -0
  40. data/lib/familia/features/external_identifier.rb +310 -0
  41. data/lib/familia/features/object_identifier.rb +307 -0
  42. data/lib/familia/features/relationships/indexing.rb +160 -175
  43. data/lib/familia/features/relationships/membership.rb +16 -21
  44. data/lib/familia/features/relationships/tracking.rb +61 -21
  45. data/lib/familia/features/relationships.rb +15 -8
  46. data/lib/familia/features/safe_dump.rb +66 -72
  47. data/lib/familia/features.rb +93 -5
  48. data/lib/familia/horreum/subclass/definition.rb +49 -3
  49. data/lib/familia/horreum.rb +15 -24
  50. data/lib/familia/secure_identifier.rb +51 -75
  51. data/lib/familia/verifiable_identifier.rb +162 -0
  52. data/lib/familia/version.rb +1 -1
  53. data/lib/familia.rb +1 -0
  54. data/setup.cfg +5 -0
  55. data/try/core/secure_identifier_try.rb +47 -18
  56. data/try/core/verifiable_identifier_try.rb +171 -0
  57. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  58. data/try/features/feature_improvements_try.rb +126 -0
  59. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  60. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  61. data/try/features/real_feature_integration_try.rb +7 -6
  62. data/try/features/relationships/relationships_api_changes_try.rb +339 -0
  63. data/try/features/relationships/relationships_try.rb +6 -5
  64. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  65. data/try/helpers/test_helpers.rb +17 -17
  66. metadata +62 -41
  67. data/examples/relationships_basic.rb +0 -273
  68. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  69. data/lib/familia/features/external_identifiers.rb +0 -111
  70. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  71. data/lib/familia/features/object_identifiers.rb +0 -194
  72. /data/docs/{wiki → guides}/API-Reference.md +0 -0
  73. /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
  74. /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
  75. /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
  76. /data/docs/{wiki → guides}/Feature-System-Guide.md +0 -0
  77. /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
  78. /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
  79. /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
  80. /data/docs/{wiki → guides}/Security-Model.md +0 -0
  81. /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Basic Relationships Example
4
+ # This example demonstrates the core features of Familia's relationships system
5
+
6
+ require 'familia'
7
+
8
+ # Configure Familia for the example
9
+ # Note: Individual models can specify logical_database if needed
10
+ Familia.configure do |config|
11
+ config.uri = 'redis://localhost:6379/'
12
+ end
13
+
14
+ puts '=== Familia Relationships Basic Example ==='
15
+ puts
16
+
17
+ # Define our model classes
18
+ class Customer < Familia::Horreum
19
+ logical_database 15 # Use test database
20
+ feature :relationships
21
+
22
+ identifier_field :custid
23
+ field :custid
24
+ field :name
25
+ field :email
26
+ field :plan
27
+
28
+ # Define collections for tracking relationships
29
+ set :domains # Simple set of domain IDs
30
+ list :projects # Ordered list of project IDs
31
+ sorted_set :activity # Activity feed with timestamps
32
+
33
+ # Create indexes for fast lookups (using class_ prefix for class-level)
34
+ class_indexed_by :email, :email_lookup # i.e. Customer.email_lookup
35
+ class_indexed_by :plan, :plan_lookup # i.e. Customer.plan_lookup
36
+
37
+ # Track in class-level collections (using class_ prefix for class-level)
38
+ class_tracked_in :all_customers, score: :created_at # i.e. Customer.all_customers
39
+ end
40
+
41
+ class Domain < Familia::Horreum
42
+ logical_database 15 # Use test database
43
+ feature :relationships
44
+
45
+ identifier_field :domain_id
46
+ field :domain_id
47
+ field :name
48
+ field :dns_zone
49
+ field :status
50
+
51
+ # Declare membership in customer collections
52
+ member_of Customer, :domains # , type: :set
53
+
54
+ # Track domains by status (using class_ prefix for class-level)
55
+ class_tracked_in :active_domains,
56
+ score: -> { status == 'active' ? Time.now.to_i : 0 }
57
+ end
58
+
59
+ class Project < Familia::Horreum
60
+ logical_database 15 # Use test database
61
+ feature :relationships
62
+
63
+ identifier_field :project_id
64
+ field :project_id
65
+ field :name
66
+ field :priority
67
+
68
+ # Member of customer projects list (ordered)
69
+ member_of Customer, :projects, type: :list
70
+ end
71
+
72
+ puts '=== 1. Basic Object Creation ==='
73
+
74
+ # Create some sample objects
75
+ customer = Customer.new(
76
+ # custid: "cust_#{SecureRandom.hex(4)}",
77
+ name: 'Acme Corporation',
78
+ email: 'admin@acme.com',
79
+ plan: 'enterprise'
80
+ )
81
+
82
+ domain1 = Domain.new(
83
+ # domain_id: "dom_#{SecureRandom.hex(4)}",
84
+ name: 'acme.com',
85
+ dns_zone: 'acme.com.',
86
+ status: 'active'
87
+ )
88
+
89
+ domain2 = Domain.new(
90
+ # domain_id: "dom_#{SecureRandom.hex(4)}",
91
+ name: 'staging.acme.com',
92
+ dns_zone: 'staging.acme.com.',
93
+ status: 'active'
94
+ )
95
+
96
+ project = Project.new(
97
+ # project_id: "proj_#{SecureRandom.hex(4)}",
98
+ name: 'Website Redesign',
99
+ priority: 'high'
100
+ )
101
+
102
+ puts "✓ Created customer: #{customer.name} (#{customer.custid})"
103
+ puts "✓ Created domains: #{domain1.name}, #{domain2.name}"
104
+ puts "✓ Created project: #{project.name}"
105
+ puts
106
+
107
+ puts '=== 2. Establishing Relationships ==='
108
+
109
+ # Save objects to automatically update indexes and class-level tracking
110
+ customer.save # Automatically adds to email_lookup, plan_lookup, and all_customers
111
+ puts '✓ Customer automatically added to indexes and tracking on save'
112
+
113
+ # Establish member_of relationships using clean << operator syntax
114
+ customer.domains << domain1 # Clean Ruby-like syntax
115
+ customer.domains << domain2 # Same as domain1.add_to_customer_domains(customer)
116
+ customer.projects << project # Same as project.add_to_customer_projects(customer)
117
+
118
+ puts '✓ Established domain ownership relationships using << operator'
119
+ puts '✓ Established project ownership relationships using << operator'
120
+
121
+ # Save domains to automatically add them to class-level tracking
122
+ domain1.save # Automatically adds to active_domains if status == 'active'
123
+ domain2.save # Automatically adds to active_domains if status == 'active'
124
+ puts '✓ Domains automatically added to status tracking on save'
125
+ puts
126
+
127
+ puts '=== 3. Querying Relationships ==='
128
+
129
+ record = Customer.get_by_email('admin@acme.com')
130
+ puts "Email lookup: #{record&.custid || 'not found'}"
131
+
132
+ Customer.get_by_plan('enterprise') # raises MoreThanOne
133
+
134
+ results = Customer.find_by_email('admin@acme.com')
135
+ puts "Email lookup: #{results&.size} found"
136
+
137
+ results = Customer.find_by_plan('enterprise')
138
+ puts "Enterprise lookup: #{results&.size} found"
139
+ puts
140
+
141
+ # Test membership queries
142
+ puts "\nDomain membership checks:"
143
+ puts " #{domain1.name} belongs to customer? #{domain1.in_customer_domains?(customer)}"
144
+ puts " #{domain2.name} belongs to customer? #{domain2.in_customer_domains?(customer)}"
145
+
146
+ puts "\nCustomer collections:"
147
+ puts " Customer has #{customer.domains.size} domains"
148
+ puts " Customer has #{customer.projects.size} projects"
149
+ puts " Domain IDs: #{customer.domains.members}"
150
+ puts " Project IDs: #{customer.projects.members}"
151
+
152
+ # Test tracked_in collections
153
+ all_customers_count = Customer.values.size
154
+ puts "\nClass-level tracking:"
155
+ puts " Total customers in system: #{all_customers_count}"
156
+
157
+ active_domains_count = Domain.active_domains.size
158
+ puts " Active domains in system: #{active_domains_count}"
159
+ puts
160
+
161
+ puts '=== 4. Range Queries ==='
162
+
163
+ # Get recent customers (last 24 hours)
164
+ yesterday = (Time.now - (24 * 3600)).to_i # 24 hours ago
165
+ recent_customers = Customer.values.rangebyscore(yesterday, '+inf')
166
+ puts "Recent customers (last 24h): #{recent_customers.size}"
167
+
168
+ # Get all active domains by score
169
+ active_domain_scores = Domain.active_domains.rangebyscore(1, '+inf', with_scores: true)
170
+ puts 'Active domains with timestamps:'
171
+ active_domain_scores.each_slice(2) do |domain_id, timestamp|
172
+ puts " #{domain_id}: active since #{Time.at(timestamp.to_i)} #{timestamp.inspect}"
173
+ end
174
+ puts
175
+
176
+ puts '=== 6. Relationship Cleanup ==='
177
+
178
+ # Remove relationships
179
+ puts 'Cleaning up relationships...'
180
+
181
+ # Remove from member_of relationships
182
+ domain1.remove_from_customer_domains(customer)
183
+ puts "✓ Removed #{domain1.name} from customer domains"
184
+
185
+ # Remove from tracking collections
186
+ Domain.active_domains.remove(domain2.identifier)
187
+ puts "✓ Removed #{domain2.name} from active domains"
188
+
189
+ # Verify cleanup
190
+ puts "\nAfter cleanup:"
191
+ puts " Customer domains: #{customer.domains.size}"
192
+ puts " Active domains: #{Domain.active_domains.size}"
193
+ puts
194
+
195
+ puts '=== Example Complete! ==='
196
+ puts
197
+ puts 'Key takeaways:'
198
+ puts '• class_tracked_in: Automatic class-level collections updated on save'
199
+ puts '• class_indexed_by: Automatic class-level indexes updated on save'
200
+ puts '• member_of: Use << operator for clean Ruby-like collection syntax'
201
+ puts '• indexed_by with parent:: Use for relationship-scoped indexes'
202
+ puts '• Save operations: Automatically update indexes and class-level tracking'
203
+ puts '• << operator: Works naturally with all collection types (sets, lists, sorted sets)'
204
+ puts
205
+ puts 'See docs/wiki/Relationships-Guide.md for comprehensive documentation'
@@ -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'
23
- spec.add_dependency 'connection_pool'
24
- spec.add_dependency 'csv'
25
- spec.add_dependency 'logger'
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
@@ -57,7 +57,6 @@ module Familia
57
57
  # Familia.connect('redis://localhost:6379')
58
58
  def connect(uri = nil)
59
59
  parsed_uri = normalize_uri(uri)
60
- serverid = parsed_uri.serverid
61
60
 
62
61
  if Familia.enable_database_logging
63
62
  DatabaseLogger.logger = Familia.logger
@@ -70,14 +69,7 @@ module Familia
70
69
  RedisClient.register(DatabaseCommandCounter)
71
70
  end
72
71
 
73
- dbclient = Redis.new(parsed_uri.conf)
74
-
75
- if @database_clients.key?(serverid)
76
- msg = "Overriding existing connection for #{serverid}"
77
- Familia.warn(msg)
78
- end
79
-
80
- @database_clients[serverid] = dbclient
72
+ Redis.new(parsed_uri.conf)
81
73
  end
82
74
 
83
75
  def reconnect(uri = nil)
@@ -86,6 +78,7 @@ module Familia
86
78
 
87
79
  # Close the existing connection if it exists
88
80
  @database_clients[serverid].close if @database_clients.key?(serverid)
81
+ @database_clients.delete(serverid)
89
82
 
90
83
  connect(parsed_uri)
91
84
  end
@@ -126,19 +119,9 @@ module Familia
126
119
 
127
120
  # Legacy behavior: create connection
128
121
  parsed_uri = normalize_uri(uri)
122
+ serverid = parsed_uri.serverid
129
123
 
130
- # Only cache when no specific URI/DB is requested to avoid DB conflicts
131
- if uri.nil?
132
- @dbclient ||= connect(parsed_uri)
133
- @dbclient.select(parsed_uri.db) if parsed_uri.db
134
- @dbclient
135
- else
136
- # When a specific DB is requested, create a new connection
137
- # to avoid conflicts with cached connections
138
- connection = connect(parsed_uri)
139
- connection.select(parsed_uri.db) if parsed_uri.db
140
- connection
141
- end
124
+ @database_clients[serverid] ||= connect(parsed_uri)
142
125
  end
143
126
 
144
127
  # Executes Database commands atomically within a transaction (MULTI/EXEC).
@@ -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