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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +2 -3
- data/CHANGELOG.rst +507 -0
- data/CLAUDE.md +5 -55
- data/Gemfile +1 -6
- data/Gemfile.lock +13 -7
- data/changelog.d/README.md +45 -34
- data/changelog.d/scriv.ini +5 -0
- 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/Feature-System-Autoloading.md +228 -0
- 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/guides/time-utilities.md +221 -0
- data/docs/migrating/.gitignore +2 -0
- data/docs/migrating/v2.0.0-pre.md +84 -0
- data/docs/migrating/v2.0.0-pre11.md +253 -0
- data/docs/migrating/v2.0.0-pre12.md +306 -0
- data/docs/migrating/v2.0.0-pre13.md +329 -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/autoloader/mega_customer/safe_dump_fields.rb +6 -0
- data/examples/autoloader/mega_customer.rb +17 -0
- 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 +5 -4
- data/lib/familia/autoloader.rb +53 -0
- data/lib/familia/base.rb +57 -0
- data/lib/familia/data_type.rb +4 -0
- data/lib/familia/encryption/encrypted_data.rb +4 -4
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +5 -0
- data/lib/familia/features/autoloadable.rb +113 -0
- data/lib/familia/features/encrypted_fields/concealed_string.rb +4 -2
- data/lib/familia/features/expiration.rb +4 -0
- data/lib/familia/features/external_identifier.rb +310 -0
- data/lib/familia/features/object_identifier.rb +307 -0
- data/lib/familia/features/quantization.rb +5 -0
- data/lib/familia/features/safe_dump.rb +74 -73
- data/lib/familia/features.rb +109 -17
- data/lib/familia/field_type.rb +2 -0
- data/lib/familia/horreum/core/serialization.rb +3 -3
- data/lib/familia/horreum/subclass/definition.rb +50 -7
- data/lib/familia/horreum.rb +2 -0
- data/lib/familia/json_serializer.rb +70 -0
- data/lib/familia/logging.rb +12 -10
- data/lib/familia/refinements/logger_trace.rb +57 -0
- data/lib/familia/refinements/snake_case.rb +40 -0
- data/lib/familia/refinements/time_utils.rb +248 -0
- data/lib/familia/refinements.rb +3 -49
- data/lib/familia/secure_identifier.rb +51 -75
- data/lib/familia/utils.rb +2 -0
- data/lib/familia/validation/{test_helpers.rb → validation_helpers.rb} +2 -2
- data/lib/familia/validation.rb +1 -1
- data/lib/familia/verifiable_identifier.rb +162 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +15 -2
- data/try/core/autoloader_try.rb +112 -0
- data/try/core/extensions_try.rb +38 -21
- data/try/core/familia_extended_try.rb +4 -3
- data/try/core/secure_identifier_try.rb +47 -18
- data/try/core/time_utils_try.rb +130 -0
- data/try/core/verifiable_identifier_try.rb +171 -0
- data/try/data_types/datatype_base_try.rb +3 -2
- data/try/features/autoloadable/autoloadable_try.rb +61 -0
- data/try/features/encrypted_fields/concealed_string_core_try.rb +8 -3
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +59 -17
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +36 -12
- data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
- data/try/features/feature_improvements_try.rb +127 -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 +8 -7
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +111 -0
- data/try/features/safe_dump/safe_dump_try.rb +8 -9
- data/try/helpers/test_helpers.rb +41 -17
- data/try/integration/cross_component_try.rb +3 -1
- metadata +61 -26
- data/CHANGELOG.md +0 -184
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +0 -29
- data/lib/familia/core_ext.rb +0 -135
- 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
- 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
|
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,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.
|
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
|
+
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
|
data/lib/familia/data_type.rb
CHANGED
@@ -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 =
|
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
|
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 =
|
35
|
-
rescue
|
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
|
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(**
|
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
|
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}"
|