familia 2.0.0.pre7 → 2.0.0.pre10
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/.github/workflows/ci.yml +13 -0
- data/.github/workflows/docs.yml +1 -1
- data/.gitignore +9 -9
- data/.rubocop.yml +19 -0
- data/.yardopts +22 -1
- data/CHANGELOG.md +184 -0
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +97 -2
- data/changelog.d/README.md +66 -0
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +29 -0
- data/docs/archive/.gitignore +2 -0
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
- data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
- data/docs/archive/FAMILIA_UPDATE.md +226 -0
- data/docs/archive/README.md +67 -0
- data/docs/guides/.gitignore +2 -0
- data/docs/{wiki → guides}/Feature-System-Guide.md +0 -15
- data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
- data/docs/guides/relationships-methods.md +266 -0
- data/examples/relationships_basic.rb +90 -157
- data/familia.gemspec +4 -4
- data/lib/familia/connection.rb +4 -21
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
- data/lib/familia/features/external_identifiers.rb +111 -0
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
- data/lib/familia/features/object_identifiers.rb +194 -0
- data/lib/familia/features/relationships/cascading.rb +0 -1
- data/lib/familia/features/relationships/indexing.rb +160 -176
- data/lib/familia/features/relationships/membership.rb +16 -22
- data/lib/familia/features/relationships/querying.rb +7 -12
- data/lib/familia/features/relationships/score_encoding.rb +1 -3
- data/lib/familia/features/relationships/tracking.rb +61 -22
- data/lib/familia/features/relationships.rb +15 -8
- data/lib/familia/features/transient_fields.rb +8 -10
- data/lib/familia/features.rb +16 -13
- data/lib/familia/horreum/core/serialization.rb +2 -5
- data/lib/familia/horreum/subclass/definition.rb +36 -0
- data/lib/familia/horreum.rb +15 -24
- data/lib/familia/version.rb +1 -3
- data/setup.cfg +12 -0
- data/try/core/errors_try.rb +1 -1
- data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
- data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
- data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
- data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
- data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
- data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
- data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
- data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
- data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
- data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +339 -0
- data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
- data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
- data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
- data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
- data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
- data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +7 -6
- data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
- data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
- data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
- data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
- metadata +80 -60
- /data/docs/{wiki → guides}/API-Reference.md +0 -0
- /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
- /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
- /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
- /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
- /data/docs/{wiki → guides}/Home.md +0 -0
- /data/docs/{wiki → guides}/Implementation-Guide.md +0 -0
- /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Security-Model.md +0 -0
- /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
@@ -34,6 +34,35 @@ module Familia
|
|
34
34
|
word.to_s.split('_').map(&:capitalize).join
|
35
35
|
end
|
36
36
|
|
37
|
+
# Define a class-level tracked collection
|
38
|
+
#
|
39
|
+
# @param collection_name [Symbol] Name of the class-level collection
|
40
|
+
# @param score [Symbol, Proc, nil] How to calculate the score
|
41
|
+
# @param on_destroy [Symbol] What to do when object is destroyed (:remove, :ignore)
|
42
|
+
#
|
43
|
+
# @example Class-level tracking (using class_ prefix convention)
|
44
|
+
# class_tracked_in :all_customers, score: :created_at
|
45
|
+
# class_tracked_in :active_users, score: -> { status == 'active' ? Time.now.to_i : 0 }
|
46
|
+
def class_tracked_in(collection_name, score: nil, on_destroy: :remove)
|
47
|
+
|
48
|
+
klass_name = (name || self.to_s).downcase
|
49
|
+
|
50
|
+
# Store metadata for this tracking relationship
|
51
|
+
tracking_relationships << {
|
52
|
+
context_class: klass_name,
|
53
|
+
context_class_name: name || self.to_s,
|
54
|
+
collection_name: collection_name,
|
55
|
+
score: score,
|
56
|
+
on_destroy: on_destroy
|
57
|
+
}
|
58
|
+
|
59
|
+
# Generate class-level collection methods
|
60
|
+
generate_tracking_class_methods(self, collection_name)
|
61
|
+
|
62
|
+
# Generate instance methods for class-level tracking
|
63
|
+
generate_tracking_instance_methods('class', collection_name, score)
|
64
|
+
end
|
65
|
+
|
37
66
|
# Define a tracked_in relationship
|
38
67
|
#
|
39
68
|
# @param context_class [Class, Symbol] The class that owns the collection
|
@@ -49,10 +78,9 @@ module Familia
|
|
49
78
|
# tracked_in Team, :domains, score: :added_at
|
50
79
|
# tracked_in Organization, :all_domains, score: :created_at
|
51
80
|
def tracked_in(context_class, collection_name, score: nil, on_destroy: :remove)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
elsif context_class.is_a?(Class)
|
81
|
+
|
82
|
+
# Handle class context
|
83
|
+
if context_class.is_a?(Class)
|
56
84
|
class_name = context_class.name
|
57
85
|
context_class_name = if class_name.include?('::')
|
58
86
|
# Extract the last part after the last ::
|
@@ -60,7 +88,6 @@ module Familia
|
|
60
88
|
else
|
61
89
|
class_name
|
62
90
|
end
|
63
|
-
# Extract just the class name, handling anonymous classes
|
64
91
|
else
|
65
92
|
context_class_name = camelize_word(context_class)
|
66
93
|
end
|
@@ -74,12 +101,8 @@ module Familia
|
|
74
101
|
on_destroy: on_destroy
|
75
102
|
}
|
76
103
|
|
77
|
-
# Generate
|
78
|
-
|
79
|
-
generate_global_class_methods(self, collection_name)
|
80
|
-
else
|
81
|
-
generate_context_class_methods(context_class, collection_name)
|
82
|
-
end
|
104
|
+
# Generate context class methods
|
105
|
+
generate_context_class_methods(context_class, collection_name)
|
83
106
|
|
84
107
|
# Generate instance methods on this class
|
85
108
|
generate_tracking_instance_methods(context_class_name, collection_name, score)
|
@@ -92,21 +115,21 @@ module Familia
|
|
92
115
|
|
93
116
|
private
|
94
117
|
|
95
|
-
# Generate
|
96
|
-
def
|
97
|
-
# Generate
|
98
|
-
target_class.define_singleton_method("
|
99
|
-
collection_key = "
|
118
|
+
# Generate class-level collection methods (e.g., User.all_users)
|
119
|
+
def generate_tracking_class_methods(target_class, collection_name)
|
120
|
+
# Generate class-level collection getter method
|
121
|
+
target_class.define_singleton_method("#{collection_name}") do
|
122
|
+
collection_key = "#{self.name.downcase}:#{collection_name}"
|
100
123
|
Familia::SortedSet.new(nil, dbkey: collection_key, logical_database: logical_database)
|
101
124
|
end
|
102
125
|
|
103
|
-
# Generate
|
126
|
+
# Generate class-level add method (e.g., User.add_to_all_users)
|
104
127
|
target_class.define_singleton_method("add_to_#{collection_name}") do |item, score = nil|
|
105
|
-
collection = send("
|
128
|
+
collection = send("#{collection_name}")
|
106
129
|
|
107
130
|
# Calculate score if not provided
|
108
131
|
score ||= if item.respond_to?(:calculate_tracking_score)
|
109
|
-
item.calculate_tracking_score(
|
132
|
+
item.calculate_tracking_score('class', collection_name)
|
110
133
|
else
|
111
134
|
item.current_score
|
112
135
|
end
|
@@ -117,9 +140,9 @@ module Familia
|
|
117
140
|
collection.add(score, item.identifier)
|
118
141
|
end
|
119
142
|
|
120
|
-
# Generate
|
143
|
+
# Generate class-level remove method
|
121
144
|
target_class.define_singleton_method("remove_from_#{collection_name}") do |item|
|
122
|
-
collection = send("
|
145
|
+
collection = send("#{collection_name}")
|
123
146
|
collection.delete(item.identifier)
|
124
147
|
end
|
125
148
|
end
|
@@ -304,6 +327,23 @@ module Familia
|
|
304
327
|
end
|
305
328
|
end
|
306
329
|
|
330
|
+
# Add to class-level tracking collections automatically
|
331
|
+
def add_to_class_tracking_collections
|
332
|
+
return unless self.class.respond_to?(:tracking_relationships)
|
333
|
+
|
334
|
+
self.class.tracking_relationships.each do |config|
|
335
|
+
context_class_name = config[:context_class_name]
|
336
|
+
context_class = config[:context_class]
|
337
|
+
collection_name = config[:collection_name]
|
338
|
+
|
339
|
+
# Only auto-add to class-level collections (where context_class matches self.class)
|
340
|
+
if context_class_name.downcase == self.class.name.downcase
|
341
|
+
# Call the class method to add this object
|
342
|
+
self.class.send("add_to_#{collection_name}", self)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
307
347
|
# Remove from all tracking collections (used during destroy)
|
308
348
|
def remove_from_all_tracking_collections
|
309
349
|
return unless self.class.respond_to?(:tracking_relationships)
|
@@ -372,7 +412,6 @@ module Familia
|
|
372
412
|
memberships
|
373
413
|
end
|
374
414
|
end
|
375
|
-
|
376
415
|
end
|
377
416
|
end
|
378
417
|
end
|
@@ -49,8 +49,8 @@ module Familia
|
|
49
49
|
# tracked_in Organization, :all_domains, score: :created_at
|
50
50
|
#
|
51
51
|
# # O(1) lookups with Redis hashes
|
52
|
-
# indexed_by :display_name,
|
53
|
-
# indexed_by :display_name,
|
52
|
+
# indexed_by :display_name, :domain_index, context: Customer
|
53
|
+
# indexed_by :display_name, :global_domain_index, context: :global
|
54
54
|
#
|
55
55
|
# # Context-aware membership (no method collisions)
|
56
56
|
# member_of Customer, :domains
|
@@ -66,7 +66,7 @@ module Familia
|
|
66
66
|
#
|
67
67
|
# # Indexing methods
|
68
68
|
# Customer.find_by_display_name(name) # O(1) lookup
|
69
|
-
# Domain.
|
69
|
+
# Domain.find_by_display_name(name) # Global lookup
|
70
70
|
#
|
71
71
|
# # Membership methods (collision-free naming)
|
72
72
|
# domain.add_to_customer_domains(customer) # Specific collection
|
@@ -264,15 +264,22 @@ module Familia
|
|
264
264
|
# This can be overridden by subclasses to set up initial relationships
|
265
265
|
end
|
266
266
|
|
267
|
-
# Override save to update relationships
|
267
|
+
# Override save to update relationships automatically
|
268
268
|
def save(update_expiration: true)
|
269
269
|
result = super
|
270
270
|
|
271
|
-
if result
|
272
|
-
#
|
273
|
-
update_all_indexes
|
271
|
+
if result
|
272
|
+
# Automatically update all indexes when object is saved
|
273
|
+
if respond_to?(:update_all_indexes)
|
274
|
+
update_all_indexes
|
275
|
+
end
|
276
|
+
|
277
|
+
# Auto-add to class-level tracking collections
|
278
|
+
if respond_to?(:add_to_class_tracking_collections)
|
279
|
+
add_to_class_tracking_collections
|
280
|
+
end
|
274
281
|
|
275
|
-
# NOTE:
|
282
|
+
# NOTE: Relationship-specific membership and tracking updates are done explicitly
|
276
283
|
# since we need to know which specific collections this object should be in
|
277
284
|
end
|
278
285
|
|
@@ -182,9 +182,7 @@ module Familia
|
|
182
182
|
def clear_transient_fields!
|
183
183
|
self.class.transient_fields.each do |field_name|
|
184
184
|
field_value = instance_variable_get("@#{field_name}")
|
185
|
-
if field_value.respond_to?(:clear!)
|
186
|
-
field_value.clear!
|
187
|
-
end
|
185
|
+
field_value.clear! if field_value.respond_to?(:clear!)
|
188
186
|
end
|
189
187
|
end
|
190
188
|
|
@@ -213,13 +211,13 @@ module Familia
|
|
213
211
|
def transient_fields_summary
|
214
212
|
self.class.transient_fields.each_with_object({}) do |field_name, summary|
|
215
213
|
field_value = instance_variable_get("@#{field_name}")
|
216
|
-
if field_value.nil?
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
214
|
+
summary[field_name] = if field_value.nil?
|
215
|
+
nil
|
216
|
+
elsif field_value.respond_to?(:cleared?) && field_value.cleared?
|
217
|
+
'[CLEARED]'
|
218
|
+
else
|
219
|
+
'[REDACTED]'
|
220
|
+
end
|
223
221
|
end
|
224
222
|
end
|
225
223
|
|
data/lib/familia/features.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# lib/familia/features.rb
|
2
2
|
|
3
3
|
module Familia
|
4
|
-
|
5
4
|
FeatureDefinition = Data.define(:name, :depends_on)
|
6
5
|
|
7
6
|
# Familia::Features
|
8
7
|
#
|
9
8
|
module Features
|
10
|
-
|
11
9
|
@features_enabled = nil
|
12
10
|
attr_reader :features_enabled
|
13
11
|
|
14
|
-
def feature(feature_name = nil)
|
12
|
+
def feature(feature_name = nil, **options)
|
15
13
|
@features_enabled ||= []
|
16
14
|
|
17
15
|
return features_enabled if feature_name.nil?
|
@@ -28,22 +26,28 @@ module Familia
|
|
28
26
|
return
|
29
27
|
end
|
30
28
|
|
31
|
-
if Familia.debug?
|
32
|
-
|
29
|
+
Familia.trace :FEATURE, nil, "#{self} includes #{feature_name.inspect}", caller(1..1) if Familia.debug?
|
30
|
+
|
31
|
+
# Check dependencies and raise error if missing
|
32
|
+
feature_def = Familia::Base.feature_definitions[feature_name]
|
33
|
+
if feature_def&.depends_on&.any?
|
34
|
+
missing = feature_def.depends_on - features_enabled
|
35
|
+
if missing.any?
|
36
|
+
raise Familia::Problem,
|
37
|
+
"Feature #{feature_name} requires missing dependencies: #{missing.join(', ')}"
|
38
|
+
end
|
33
39
|
end
|
34
40
|
|
35
41
|
# Add it to the list available features_enabled for Familia::Base classes.
|
36
42
|
features_enabled << feature_name
|
37
43
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
feature_def = Familia::Base.feature_definitions[feature_name]
|
42
|
-
if feature_def&.depends_on&.any?
|
43
|
-
missing = feature_def.depends_on - features_enabled
|
44
|
-
raise Familia::Problem, "#{feature_name} requires: #{missing.join(', ')}" if missing.any?
|
44
|
+
# Store feature options if any were provided using the new pattern
|
45
|
+
if options.any?
|
46
|
+
add_feature_options(feature_name, **options)
|
45
47
|
end
|
46
48
|
|
49
|
+
klass = Familia::Base.features_available[feature_name]
|
50
|
+
|
47
51
|
# Extend the Familia::Base subclass (e.g. Customer) with the feature module
|
48
52
|
include klass
|
49
53
|
|
@@ -58,7 +62,6 @@ module Familia
|
|
58
62
|
# We'd need to extend the DataType instances for each Horreum subclass. That
|
59
63
|
# avoids it getting included multiple times per DataType
|
60
64
|
end
|
61
|
-
|
62
65
|
end
|
63
66
|
end
|
64
67
|
|
@@ -135,7 +135,7 @@ module Familia
|
|
135
135
|
multi.hmset(dbkey, to_h_for_storage)
|
136
136
|
end
|
137
137
|
|
138
|
-
result.is_a?(Array)
|
138
|
+
result.is_a?(Array) # transaction succeeded
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
@@ -463,9 +463,7 @@ module Familia
|
|
463
463
|
#
|
464
464
|
def serialize_value(val)
|
465
465
|
# Security: Handle ConcealedString safely - extract encrypted data for storage
|
466
|
-
if val.respond_to?(:encrypted_value)
|
467
|
-
return val.encrypted_value
|
468
|
-
end
|
466
|
+
return val.encrypted_value if val.respond_to?(:encrypted_value)
|
469
467
|
|
470
468
|
prepared = Familia.distinguisher(val, strict_values: false)
|
471
469
|
|
@@ -530,6 +528,5 @@ module Familia
|
|
530
528
|
end
|
531
529
|
end
|
532
530
|
end
|
533
|
-
|
534
531
|
end
|
535
532
|
end
|
@@ -177,6 +177,8 @@ module Familia
|
|
177
177
|
# configuration values. This is particularly useful when mapping
|
178
178
|
# familia models with specific database numbers in the configuration.
|
179
179
|
#
|
180
|
+
# Familia::Horreum::DefinitionMethods#config_name
|
181
|
+
#
|
180
182
|
# @example V2::Session.config_name => 'session'
|
181
183
|
#
|
182
184
|
# @return [String] The underscored class name as a string
|
@@ -235,6 +237,40 @@ module Familia
|
|
235
237
|
field_types[field_type.name] = field_type
|
236
238
|
end
|
237
239
|
|
240
|
+
# Get feature options for a specific feature or all features
|
241
|
+
#
|
242
|
+
# @param feature_name [Symbol, nil] The feature name to get options for
|
243
|
+
# @return [Hash] The options hash for the feature, or empty hash if none
|
244
|
+
#
|
245
|
+
def feature_options(feature_name = nil)
|
246
|
+
@feature_options ||= {}
|
247
|
+
return @feature_options if feature_name.nil?
|
248
|
+
|
249
|
+
@feature_options[feature_name.to_sym] || {}
|
250
|
+
end
|
251
|
+
|
252
|
+
# Add feature options for a specific feature
|
253
|
+
#
|
254
|
+
# This method provides a clean way for features to set their default options
|
255
|
+
# without worrying about initialization state. Similar to register_field_type
|
256
|
+
# for field types.
|
257
|
+
#
|
258
|
+
# @param feature_name [Symbol] The feature name
|
259
|
+
# @param options [Hash] The options to add/merge
|
260
|
+
# @return [Hash] The updated options for the feature
|
261
|
+
#
|
262
|
+
def add_feature_options(feature_name, **options)
|
263
|
+
@feature_options ||= {}
|
264
|
+
@feature_options[feature_name.to_sym] ||= {}
|
265
|
+
|
266
|
+
# Only set defaults for options that don't already exist
|
267
|
+
options.each do |key, value|
|
268
|
+
@feature_options[feature_name.to_sym][key] ||= value
|
269
|
+
end
|
270
|
+
|
271
|
+
@feature_options[feature_name.to_sym]
|
272
|
+
end
|
273
|
+
|
238
274
|
# Create and register a transient field type
|
239
275
|
#
|
240
276
|
# @param name [Symbol] The field name
|
data/lib/familia/horreum.rb
CHANGED
@@ -104,39 +104,30 @@ module Familia
|
|
104
104
|
args = []
|
105
105
|
end
|
106
106
|
|
107
|
-
# Initialize object with arguments using one of
|
107
|
+
# Initialize object with arguments using one of four strategies:
|
108
108
|
#
|
109
|
-
# 1. **
|
110
|
-
# Example: Customer.new(
|
111
|
-
# - Robust
|
112
|
-
# - Self-documenting
|
113
|
-
# - Only sets provided fields
|
109
|
+
# 1. **Identifier** (Recommended for lookups): A single argument is treated as the identifier.
|
110
|
+
# Example: Customer.new("cust_123")
|
111
|
+
# - Robust and convenient for creating objects from an ID.
|
114
112
|
#
|
115
|
-
# 2. **
|
116
|
-
# Example: Customer.new("john@example.com"
|
117
|
-
# - Brittle: breaks if field order changes
|
118
|
-
# - Compact syntax
|
119
|
-
# - Maps to fields in class definition order
|
113
|
+
# 2. **Keyword Arguments** (Recommended for creation): Order-independent field assignment
|
114
|
+
# Example: Customer.new(name: "John", email: "john@example.com")
|
120
115
|
#
|
121
|
-
# 3. **
|
122
|
-
#
|
123
|
-
# - Fields set on-demand via accessors or save()
|
124
|
-
# - Avoids default value conflicts with nil-skipping serialization
|
116
|
+
# 3. **Positional Arguments** (Legacy): Field assignment by definition order
|
117
|
+
# Example: Customer.new("cust_123", "John", "john@example.com")
|
125
118
|
#
|
126
|
-
#
|
127
|
-
# defined fields are set, preventing typos from creating undefined attributes.
|
119
|
+
# 4. **No Arguments**: Object created with all fields as nil
|
128
120
|
#
|
129
|
-
if kwargs.
|
121
|
+
if args.size == 1 && kwargs.empty?
|
122
|
+
id_field = self.class.identifier_field
|
123
|
+
send(:"#{id_field}=", args.first)
|
124
|
+
elsif kwargs.any?
|
130
125
|
initialize_with_keyword_args(**kwargs)
|
131
126
|
elsif args.any?
|
132
127
|
initialize_with_positional_args(*args)
|
133
128
|
else
|
134
|
-
|
135
|
-
# Default values are intentionally NOT set here
|
136
|
-
# - Maintain Database memory efficiency (only store non-nil values)
|
137
|
-
# - Avoid conflicts with nil-skipping serialization logic
|
138
|
-
# - Preserve consistent exists? behavior (empty vs default-filled objects)
|
139
|
-
# - Keep initialization lightweight for unused fields
|
129
|
+
Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
|
130
|
+
# Default values are intentionally NOT set here
|
140
131
|
end
|
141
132
|
|
142
133
|
# Implementing classes can define an init method to do any
|
data/lib/familia/version.rb
CHANGED
data/setup.cfg
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
[scriv]
|
2
|
+
format = md
|
3
|
+
categories = Added, Changed, Deprecated, Removed, Fixed, Security, Documentation, AI Assistance
|
4
|
+
version_scheme = semver
|
5
|
+
entry_title_template = [{{ version }}] - {{ date }}
|
6
|
+
fragment_directory = changelog.d/fragments
|
7
|
+
template_file = changelog.d/template.md.j2
|
8
|
+
output_file = CHANGELOG.md
|
9
|
+
main_branches = main, develop
|
10
|
+
md_header_level = 2
|
11
|
+
end_marker = scriv-end-here
|
12
|
+
start_marker = scriv-insert-here
|
data/try/core/errors_try.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
# Security tests for the no-cache encryption strategy
|
4
4
|
# These tests verify that we maintain security properties by NOT caching derived keys
|
5
5
|
|
6
|
-
require_relative '
|
6
|
+
require_relative '../../helpers/test_helpers'
|
7
7
|
|
8
8
|
test_keys = {
|
9
9
|
v1: Base64.strict_encode64('a' * 32),
|