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.
- 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 +247 -0
- data/CLAUDE.md +12 -59
- data/Gemfile.lock +1 -1
- data/README.md +62 -2
- data/changelog.d/README.md +77 -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 +63 -0
- data/docs/guides/.gitignore +2 -0
- data/docs/{wiki → guides}/Home.md +1 -1
- data/docs/{wiki → guides}/Implementation-Guide.md +1 -1
- data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
- data/docs/guides/relationships-methods.md +266 -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 +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.rb +205 -0
- data/examples/safe_dump.rb +281 -0
- data/familia.gemspec +4 -4
- data/lib/familia/base.rb +52 -0
- data/lib/familia/connection.rb +4 -21
- 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/relationships/indexing.rb +160 -175
- data/lib/familia/features/relationships/membership.rb +16 -21
- data/lib/familia/features/relationships/tracking.rb +61 -21
- data/lib/familia/features/relationships.rb +15 -8
- data/lib/familia/features/safe_dump.rb +66 -72
- data/lib/familia/features.rb +93 -5
- data/lib/familia/horreum/subclass/definition.rb +49 -3
- data/lib/familia/horreum.rb +15 -24
- 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 +5 -0
- 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/relationships/relationships_api_changes_try.rb +339 -0
- data/try/features/relationships/relationships_try.rb +6 -5
- data/try/features/safe_dump/safe_dump_try.rb +8 -9
- data/try/helpers/test_helpers.rb +17 -17
- metadata +62 -41
- data/examples/relationships_basic.rb +0 -273
- 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/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}/Feature-System-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}/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
@@ -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)
|
@@ -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
|
|
@@ -1,12 +1,18 @@
|
|
1
1
|
# lib/familia/features/safe_dump.rb
|
2
2
|
|
3
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
4
|
+
#
|
5
|
+
# Class instance variables are used here for feature configuration
|
6
|
+
# (e.g., @dump_method, @load_method). These are set once and not mutated
|
7
|
+
# at runtime, so thread safety is not a concern for this feature.
|
8
|
+
#
|
3
9
|
module Familia::Features
|
4
10
|
# SafeDump is a mixin that allows models to define a list of fields that are
|
5
11
|
# safe to dump. This is useful for serializing objects to JSON or other
|
6
12
|
# formats where you want to ensure that only certain fields are exposed.
|
7
13
|
#
|
8
|
-
# To use SafeDump, include it in your model and
|
9
|
-
#
|
14
|
+
# To use SafeDump, include it in your model and use the DSL methods to define
|
15
|
+
# safe dump fields. The fields can be either symbols or hashes. If a field is
|
10
16
|
# a symbol, the method with the same name will be called on the object to
|
11
17
|
# retrieve the value. If the field is a hash, the key is the field name and
|
12
18
|
# the value is a lambda that will be called with the object as an argument.
|
@@ -19,97 +25,85 @@ module Familia::Features
|
|
19
25
|
#
|
20
26
|
# feature :safe_dump
|
21
27
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# { :active => ->(obj) { obj.active? } }
|
27
|
-
# ]
|
28
|
+
# safe_dump_field :objid
|
29
|
+
# safe_dump_field :updated
|
30
|
+
# safe_dump_field :created
|
31
|
+
# safe_dump_field :active, ->(obj) { obj.active? }
|
28
32
|
#
|
29
|
-
#
|
30
|
-
# @safe_dump_field_map. `SafeDump.safe_dump_fields` returns only the list
|
31
|
-
# of symbols in the order they were defined. From the example above, it would
|
32
|
-
# return `[:objid, :updated, :created, :active]`.
|
33
|
-
#
|
34
|
-
# Standalone Usage:
|
35
|
-
#
|
36
|
-
# You can also use SafeDump by including it in your model and defining the
|
37
|
-
# safe dump fields using the class instance variable `@safe_dump_fields`.
|
38
|
-
#
|
39
|
-
# Example:
|
33
|
+
# Alternatively, you can define multiple fields at once:
|
40
34
|
#
|
41
|
-
#
|
42
|
-
#
|
35
|
+
# safe_dump_fields :objid, :updated, :created,
|
36
|
+
# { active: ->(obj) { obj.active? } }
|
43
37
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
# end
|
38
|
+
# Internally, all fields are normalized to the hash syntax and stored in
|
39
|
+
# @safe_dump_field_map. `SafeDump.safe_dump_fields` returns only the list
|
40
|
+
# of symbols in the order they were defined.
|
48
41
|
#
|
49
42
|
module SafeDump
|
50
43
|
@dump_method = :to_json
|
51
44
|
@load_method = :from_json
|
52
45
|
|
53
|
-
@safe_dump_fields = []
|
54
|
-
@safe_dump_field_map = {}
|
55
|
-
|
56
46
|
def self.included(base)
|
57
|
-
Familia.trace
|
47
|
+
Familia.trace(:LOADED, self, base, caller(1..1)) if Familia.debug?
|
58
48
|
base.extend ClassMethods
|
59
49
|
|
60
|
-
#
|
61
|
-
# sure we always have an array to work with.
|
62
|
-
base.instance_variable_set(:@safe_dump_fields, []) unless base.instance_variable_defined?(:@safe_dump_fields)
|
63
|
-
|
64
|
-
# Ditto for the field map
|
65
|
-
return if base.instance_variable_defined?(:@safe_dump_field_map)
|
66
|
-
|
50
|
+
# Initialize the safe dump field map
|
67
51
|
base.instance_variable_set(:@safe_dump_field_map, {})
|
68
52
|
end
|
69
53
|
|
54
|
+
# SafeDump::ClassMethods
|
55
|
+
#
|
56
|
+
# These methods become available on the model class
|
70
57
|
module ClassMethods
|
71
|
-
|
72
|
-
|
58
|
+
# Define a single safe dump field
|
59
|
+
# @param field_name [Symbol] The name of the field
|
60
|
+
# @param callable [Proc, nil] Optional callable to transform the value
|
61
|
+
def safe_dump_field(field_name, callable = nil)
|
62
|
+
@safe_dump_field_map ||= {}
|
63
|
+
|
64
|
+
field_name = field_name.to_sym
|
65
|
+
field_value = callable || lambda { |obj|
|
66
|
+
if obj.respond_to?(:[]) && obj[field_name]
|
67
|
+
obj[field_name] # Familia::DataType classes
|
68
|
+
elsif obj.respond_to?(field_name)
|
69
|
+
obj.send(field_name) # Regular method calls
|
70
|
+
end
|
71
|
+
}
|
72
|
+
|
73
|
+
@safe_dump_field_map[field_name] = field_value
|
73
74
|
end
|
74
75
|
|
75
|
-
#
|
76
|
-
#
|
77
|
-
def safe_dump_fields
|
78
|
-
|
79
|
-
|
76
|
+
# Define multiple safe dump fields at once
|
77
|
+
# @param fields [Array] Mixed array of symbols and hashes
|
78
|
+
def safe_dump_fields(*fields)
|
79
|
+
# If no arguments, return field names (getter behavior)
|
80
|
+
return safe_dump_field_names if fields.empty?
|
81
|
+
|
82
|
+
# Otherwise, define fields (setter behavior)
|
83
|
+
fields.each do |field|
|
84
|
+
if field.is_a?(Symbol)
|
85
|
+
safe_dump_field(field)
|
86
|
+
elsif field.is_a?(Hash)
|
87
|
+
field.each do |name, callable|
|
88
|
+
safe_dump_field(name, callable)
|
89
|
+
end
|
90
|
+
end
|
80
91
|
end
|
81
92
|
end
|
82
93
|
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
#
|
89
|
-
#
|
94
|
+
# Returns an array of safe dump field names in the order they were defined
|
95
|
+
def safe_dump_field_names
|
96
|
+
(@safe_dump_field_map || {}).keys
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the field map used for dumping
|
90
100
|
def safe_dump_field_map
|
91
|
-
|
101
|
+
@safe_dump_field_map || {}
|
102
|
+
end
|
92
103
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
# method returns only the symbols).
|
97
|
-
@safe_dump_field_map = @safe_dump_fields.each_with_object({}) do |el, map|
|
98
|
-
if el.is_a?(Symbol)
|
99
|
-
field_name = el
|
100
|
-
callable = lambda { |obj|
|
101
|
-
if obj.respond_to?(:[]) && obj[field_name]
|
102
|
-
obj[field_name] # Familia::DataType classes
|
103
|
-
elsif obj.respond_to?(field_name)
|
104
|
-
obj.send(field_name) # Onetime::Models::RedisHash classes via method_missing 😩
|
105
|
-
end
|
106
|
-
}
|
107
|
-
else
|
108
|
-
field_name = el.keys.first
|
109
|
-
callable = el.values.first
|
110
|
-
end
|
111
|
-
map[field_name] = callable
|
112
|
-
end
|
104
|
+
# Legacy method for setting safe dump fields (for backward compatibility)
|
105
|
+
def set_safe_dump_fields(*fields)
|
106
|
+
safe_dump_fields(*fields)
|
113
107
|
end
|
114
108
|
end
|
115
109
|
|
@@ -155,5 +149,5 @@ module Familia::Features
|
|
155
149
|
|
156
150
|
Familia::Base.add_feature self, :safe_dump
|
157
151
|
end
|
158
|
-
# end SafeDump
|
159
152
|
end
|
153
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
data/lib/familia/features.rb
CHANGED
@@ -5,18 +5,108 @@ module Familia
|
|
5
5
|
|
6
6
|
# Familia::Features
|
7
7
|
#
|
8
|
+
# This module provides the feature system for Familia classes. Features are
|
9
|
+
# modular capabilities that can be mixed into classes with configurable options.
|
10
|
+
# Features provide a powerful way to:
|
11
|
+
#
|
12
|
+
# - **Add new methods**: Both class and instance methods can be added
|
13
|
+
# - **Override existing methods**: Extend or replace default behavior
|
14
|
+
# - **Add new fields**: Define additional data storage capabilities
|
15
|
+
# - **Manage complexity**: Large, complex model classes can use features to
|
16
|
+
# organize functionality into focused, reusable modules
|
17
|
+
#
|
18
|
+
# ## Feature Options Storage
|
19
|
+
#
|
20
|
+
# Feature options are stored **per-class** using class-level instance variables.
|
21
|
+
# This means each Familia::Horreum subclass maintains its own isolated set of
|
22
|
+
# feature options. When you enable a feature with options in different models,
|
23
|
+
# each model stores its own separate configuration without interference.
|
24
|
+
#
|
25
|
+
# ## Project Organization with Autoloader
|
26
|
+
#
|
27
|
+
# For large projects, use {Familia::Features::Autoloader} to automatically load
|
28
|
+
# project-specific features from a dedicated directory structure. This helps
|
29
|
+
# organize complex models by separating features into individual files.
|
30
|
+
#
|
31
|
+
# @example Different models with different feature options
|
32
|
+
# class UserModel < Familia::Horreum
|
33
|
+
# feature :object_identifier, generator: :uuid_v4
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# class SessionModel < Familia::Horreum
|
37
|
+
# feature :object_identifier, generator: :hex
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# UserModel.feature_options(:object_identifier) #=> {generator: :uuid_v4}
|
41
|
+
# SessionModel.feature_options(:object_identifier) #=> {generator: :hex}
|
42
|
+
#
|
43
|
+
# @example Using features for complexity management
|
44
|
+
# class ComplexModel < Familia::Horreum
|
45
|
+
# # Organize functionality using features
|
46
|
+
# feature :expiration # TTL management
|
47
|
+
# feature :safe_dump # API-safe serialization
|
48
|
+
# feature :relationships # CRUD operations for related objects
|
49
|
+
# feature :custom_validation # Project-specific validation logic
|
50
|
+
# feature :audit_trail # Change tracking
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @example Project-specific features with autoloader
|
54
|
+
# # In your model file: app/models/customer.rb
|
55
|
+
# class Customer < Familia::Horreum
|
56
|
+
# module Features
|
57
|
+
# include Familia::Features::Autoloader
|
58
|
+
# # Automatically loads all .rb files from app/models/customer/features/
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @see Familia::Features::Autoloader For automatic feature loading
|
63
|
+
#
|
8
64
|
module Features
|
9
65
|
@features_enabled = nil
|
10
66
|
attr_reader :features_enabled
|
11
67
|
|
68
|
+
# Enables a feature for the current class with optional configuration.
|
69
|
+
#
|
70
|
+
# Features are modular capabilities that can be mixed into Familia::Horreum
|
71
|
+
# classes. Each feature can be configured with options that are stored
|
72
|
+
# **per-class**, ensuring complete isolation between different models.
|
73
|
+
#
|
74
|
+
# @param feature_name [Symbol, String, nil] the name of the feature to enable.
|
75
|
+
# If nil, returns the list of currently enabled features.
|
76
|
+
# @param options [Hash] configuration options for the feature. These are
|
77
|
+
# stored per-class and do not interfere with other models' configurations.
|
78
|
+
# @return [Array, nil] the list of enabled features if feature_name is nil,
|
79
|
+
# otherwise nil
|
80
|
+
#
|
81
|
+
# @example Enable feature without options
|
82
|
+
# class User < Familia::Horreum
|
83
|
+
# feature :expiration
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# @example Enable feature with options (per-class storage)
|
87
|
+
# class User < Familia::Horreum
|
88
|
+
# feature :object_identifier, generator: :uuid_v4
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# class Session < Familia::Horreum
|
92
|
+
# feature :object_identifier, generator: :hex # Different options
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# # Each class maintains separate options:
|
96
|
+
# User.feature_options(:object_identifier) #=> {generator: :uuid_v4}
|
97
|
+
# Session.feature_options(:object_identifier) #=> {generator: :hex}
|
98
|
+
#
|
99
|
+
# @raise [Familia::Problem] if the feature is not supported
|
100
|
+
#
|
12
101
|
def feature(feature_name = nil, **options)
|
13
102
|
@features_enabled ||= []
|
14
103
|
|
15
104
|
return features_enabled if feature_name.nil?
|
16
105
|
|
17
|
-
# If there's a value
|
106
|
+
# If there's a value provided check that it's a valid feature
|
18
107
|
feature_name = feature_name.to_sym
|
19
|
-
|
108
|
+
feature_class = Familia::Base.find_feature(feature_name, self)
|
109
|
+
unless feature_class
|
20
110
|
raise Familia::Problem, "Unsupported feature: #{feature_name}"
|
21
111
|
end
|
22
112
|
|
@@ -46,10 +136,8 @@ module Familia
|
|
46
136
|
add_feature_options(feature_name, **options)
|
47
137
|
end
|
48
138
|
|
49
|
-
klass = Familia::Base.features_available[feature_name]
|
50
|
-
|
51
139
|
# Extend the Familia::Base subclass (e.g. Customer) with the feature module
|
52
|
-
include
|
140
|
+
include feature_class
|
53
141
|
|
54
142
|
# NOTE: Do we want to extend Familia::DataType here? That would make it
|
55
143
|
# possible to call safe_dump on relations fields (e.g. list, zset, hashkey).
|
@@ -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,10 +237,36 @@ module Familia
|
|
235
237
|
field_types[field_type.name] = field_type
|
236
238
|
end
|
237
239
|
|
238
|
-
#
|
240
|
+
# Retrieves feature options for the current class.
|
241
|
+
#
|
242
|
+
# Feature options are stored **per-class** in instance variables, ensuring
|
243
|
+
# complete isolation between different Familia::Horreum subclasses. Each
|
244
|
+
# class maintains its own @feature_options hash that does not interfere
|
245
|
+
# with other classes' configurations.
|
246
|
+
#
|
247
|
+
# @param feature_name [Symbol, String, nil] the name of the feature to get options for.
|
248
|
+
# If nil, returns the entire feature options hash for this class.
|
249
|
+
# @return [Hash] the feature options hash, either for a specific feature or all features
|
250
|
+
#
|
251
|
+
# @example Getting options for a specific feature
|
252
|
+
# class MyModel < Familia::Horreum
|
253
|
+
# feature :object_identifier, generator: :uuid_v4
|
254
|
+
# end
|
255
|
+
#
|
256
|
+
# MyModel.feature_options(:object_identifier) #=> {generator: :uuid_v4}
|
257
|
+
# MyModel.feature_options #=> {object_identifier: {generator: :uuid_v4}}
|
258
|
+
#
|
259
|
+
# @example Per-class isolation
|
260
|
+
# class UserModel < Familia::Horreum
|
261
|
+
# feature :object_identifier, generator: :uuid_v4
|
262
|
+
# end
|
239
263
|
#
|
240
|
-
#
|
241
|
-
#
|
264
|
+
# class SessionModel < Familia::Horreum
|
265
|
+
# feature :object_identifier, generator: :hex
|
266
|
+
# end
|
267
|
+
#
|
268
|
+
# UserModel.feature_options(:object_identifier) #=> {generator: :uuid_v4}
|
269
|
+
# SessionModel.feature_options(:object_identifier) #=> {generator: :hex}
|
242
270
|
#
|
243
271
|
def feature_options(feature_name = nil)
|
244
272
|
@feature_options ||= {}
|
@@ -253,10 +281,28 @@ module Familia
|
|
253
281
|
# without worrying about initialization state. Similar to register_field_type
|
254
282
|
# for field types.
|
255
283
|
#
|
284
|
+
# Feature options are stored at the **class level** using instance variables,
|
285
|
+
# ensuring complete isolation between different Familia::Horreum subclasses.
|
286
|
+
# Each class maintains its own @feature_options hash.
|
287
|
+
#
|
256
288
|
# @param feature_name [Symbol] The feature name
|
257
289
|
# @param options [Hash] The options to add/merge
|
258
290
|
# @return [Hash] The updated options for the feature
|
259
291
|
#
|
292
|
+
# @note This method only sets defaults for options that don't already exist,
|
293
|
+
# using the ||= operator to prevent overwrites.
|
294
|
+
#
|
295
|
+
# @example Per-class storage behavior
|
296
|
+
# class ModelA < Familia::Horreum
|
297
|
+
# # This stores options in ModelA's @feature_options
|
298
|
+
# add_feature_options(:my_feature, key: 'value_a')
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
# class ModelB < Familia::Horreum
|
302
|
+
# # This stores options in ModelB's @feature_options (separate from ModelA)
|
303
|
+
# add_feature_options(:my_feature, key: 'value_b')
|
304
|
+
# end
|
305
|
+
#
|
260
306
|
def add_feature_options(feature_name, **options)
|
261
307
|
@feature_options ||= {}
|
262
308
|
@feature_options[feature_name.to_sym] ||= {}
|
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
|