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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -12
- data/CLAUDE.md +4 -54
- data/Gemfile.lock +1 -1
- data/changelog.d/README.md +45 -34
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +1 -1
- data/docs/archive/FAMILIA_UPDATE.md +1 -1
- data/docs/archive/README.md +15 -19
- data/docs/guides/Home.md +1 -1
- data/docs/guides/Implementation-Guide.md +1 -1
- data/docs/guides/relationships-methods.md +1 -1
- data/docs/migrating/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre.md +84 -0
- data/docs/migrating/v2.0.0-pre11.md +255 -0
- data/docs/migrating/v2.0.0-pre12.md +306 -0
- data/docs/migrating/v2.0.0-pre5.md +110 -0
- data/docs/migrating/v2.0.0-pre6.md +154 -0
- data/docs/migrating/v2.0.0-pre7.md +222 -0
- data/docs/overview.md +6 -7
- data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
- data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
- data/examples/{relationships_basic.rb → relationships.rb} +2 -3
- data/examples/safe_dump.rb +281 -0
- data/familia.gemspec +4 -4
- data/lib/familia/base.rb +52 -0
- data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
- data/lib/familia/errors.rb +2 -0
- data/lib/familia/features/autoloader.rb +57 -0
- data/lib/familia/features/external_identifier.rb +310 -0
- data/lib/familia/features/object_identifier.rb +307 -0
- data/lib/familia/features/safe_dump.rb +66 -72
- data/lib/familia/features.rb +93 -5
- data/lib/familia/horreum/subclass/definition.rb +47 -3
- data/lib/familia/secure_identifier.rb +51 -75
- data/lib/familia/verifiable_identifier.rb +162 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -0
- data/setup.cfg +1 -8
- data/try/core/secure_identifier_try.rb +47 -18
- data/try/core/verifiable_identifier_try.rb +171 -0
- data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
- data/try/features/feature_improvements_try.rb +126 -0
- data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
- data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
- data/try/features/real_feature_integration_try.rb +7 -6
- data/try/features/safe_dump/safe_dump_try.rb +8 -9
- data/try/helpers/test_helpers.rb +17 -17
- metadata +30 -22
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +0 -29
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
- data/lib/familia/features/external_identifiers.rb +0 -111
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
- 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
|
20
|
+
field :role # admin, editor, viewer, guest
|
21
21
|
field :created_at
|
22
22
|
|
23
|
-
sorted_set :documents
|
24
|
-
sorted_set :recent_activity
|
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
|
42
|
+
field :document_type # public, private, confidential
|
43
43
|
|
44
|
-
sorted_set :collaborators
|
45
|
-
list :audit_log
|
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 |
|
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: [
|
117
|
-
editor: [
|
118
|
-
reviewer: [
|
119
|
-
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)
|
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
|
177
|
-
puts
|
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,
|
186
|
-
doc2 = DocumentService.create_document(alice,
|
187
|
-
doc3 = DocumentService.create_document(bob,
|
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
|
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, [
|
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
|
228
|
+
[doc1, doc2, doc3].each do |doc|
|
229
|
+
doc.clear_all_permissions
|
230
|
+
doc.collaborators.clear
|
231
|
+
end
|
229
232
|
|
230
|
-
puts
|
233
|
+
puts '✅ Integration example completed successfully!'
|
231
234
|
puts "\nThis demonstrates:"
|
232
|
-
puts
|
233
|
-
puts
|
234
|
-
puts
|
235
|
-
puts
|
236
|
-
puts
|
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
|
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
|
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.
|
23
|
-
spec.add_dependency 'connection_pool', '~> 2.
|
24
|
-
spec.add_dependency 'csv', '~> 3.
|
25
|
-
spec.add_dependency 'logger', '~> 1.
|
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
|
data/lib/familia/errors.rb
CHANGED
@@ -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
|