familia 2.0.0.pre8 → 2.0.0.pre12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -0
  3. data/.github/workflows/docs.yml +1 -1
  4. data/.gitignore +9 -9
  5. data/.rubocop.yml +19 -0
  6. data/.yardopts +22 -1
  7. data/CHANGELOG.md +247 -0
  8. data/CLAUDE.md +12 -59
  9. data/Gemfile.lock +1 -1
  10. data/README.md +62 -2
  11. data/changelog.d/README.md +77 -0
  12. data/docs/archive/.gitignore +2 -0
  13. data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
  14. data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
  15. data/docs/archive/FAMILIA_UPDATE.md +226 -0
  16. data/docs/archive/README.md +63 -0
  17. data/docs/guides/.gitignore +2 -0
  18. data/docs/{wiki → guides}/Home.md +1 -1
  19. data/docs/{wiki → guides}/Implementation-Guide.md +1 -1
  20. data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
  21. data/docs/guides/relationships-methods.md +266 -0
  22. data/docs/migrating/.gitignore +2 -0
  23. data/docs/migrating/v2.0.0-pre.md +84 -0
  24. data/docs/migrating/v2.0.0-pre11.md +255 -0
  25. data/docs/migrating/v2.0.0-pre12.md +306 -0
  26. data/docs/migrating/v2.0.0-pre5.md +110 -0
  27. data/docs/migrating/v2.0.0-pre6.md +154 -0
  28. data/docs/migrating/v2.0.0-pre7.md +222 -0
  29. data/docs/overview.md +6 -7
  30. data/{examples/redis_command_validation_example.rb → docs/reference/auditing_database_commands.rb} +29 -32
  31. data/examples/{bit_encoding_integration.rb → permissions.rb} +30 -27
  32. data/examples/relationships.rb +205 -0
  33. data/examples/safe_dump.rb +281 -0
  34. data/familia.gemspec +4 -4
  35. data/lib/familia/base.rb +52 -0
  36. data/lib/familia/connection.rb +4 -21
  37. data/lib/familia/{encryption_request_cache.rb → encryption/request_cache.rb} +1 -1
  38. data/lib/familia/errors.rb +2 -0
  39. data/lib/familia/features/autoloader.rb +57 -0
  40. data/lib/familia/features/external_identifier.rb +310 -0
  41. data/lib/familia/features/object_identifier.rb +307 -0
  42. data/lib/familia/features/relationships/indexing.rb +160 -175
  43. data/lib/familia/features/relationships/membership.rb +16 -21
  44. data/lib/familia/features/relationships/tracking.rb +61 -21
  45. data/lib/familia/features/relationships.rb +15 -8
  46. data/lib/familia/features/safe_dump.rb +66 -72
  47. data/lib/familia/features.rb +93 -5
  48. data/lib/familia/horreum/subclass/definition.rb +49 -3
  49. data/lib/familia/horreum.rb +15 -24
  50. data/lib/familia/secure_identifier.rb +51 -75
  51. data/lib/familia/verifiable_identifier.rb +162 -0
  52. data/lib/familia/version.rb +1 -1
  53. data/lib/familia.rb +1 -0
  54. data/setup.cfg +5 -0
  55. data/try/core/secure_identifier_try.rb +47 -18
  56. data/try/core/verifiable_identifier_try.rb +171 -0
  57. data/try/features/{external_identifiers/external_identifiers_try.rb → external_identifier/external_identifier_try.rb} +25 -28
  58. data/try/features/feature_improvements_try.rb +126 -0
  59. data/try/features/{object_identifiers/object_identifiers_integration_try.rb → object_identifier/object_identifier_integration_try.rb} +28 -30
  60. data/try/features/{object_identifiers/object_identifiers_try.rb → object_identifier/object_identifier_try.rb} +13 -13
  61. data/try/features/real_feature_integration_try.rb +7 -6
  62. data/try/features/relationships/relationships_api_changes_try.rb +339 -0
  63. data/try/features/relationships/relationships_try.rb +6 -5
  64. data/try/features/safe_dump/safe_dump_try.rb +8 -9
  65. data/try/helpers/test_helpers.rb +17 -17
  66. metadata +62 -41
  67. data/examples/relationships_basic.rb +0 -273
  68. data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +0 -120
  69. data/lib/familia/features/external_identifiers.rb +0 -111
  70. data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +0 -91
  71. data/lib/familia/features/object_identifiers.rb +0 -194
  72. /data/docs/{wiki → guides}/API-Reference.md +0 -0
  73. /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
  74. /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
  75. /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
  76. /data/docs/{wiki → guides}/Feature-System-Guide.md +0 -0
  77. /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
  78. /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
  79. /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
  80. /data/docs/{wiki → guides}/Security-Model.md +0 -0
  81. /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
@@ -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
- # Handle special :global context
53
- if context_class == :global
54
- context_class_name = 'Global'
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 class methods on the context class (skip for global)
78
- if context_class == :global
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 global collection methods (e.g., Domain.global_all_domains)
96
- def generate_global_class_methods(target_class, collection_name)
97
- # Generate global collection getter method
98
- target_class.define_singleton_method("global_#{collection_name}") do
99
- collection_key = "global:#{collection_name}"
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 global add method (e.g., Domain.add_to_global_all_domains)
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("global_#{collection_name}")
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(:global, collection_name)
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 global remove method
143
+ # Generate class-level remove method
121
144
  target_class.define_singleton_method("remove_from_#{collection_name}") do |item|
122
- collection = send("global_#{collection_name}")
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, in: Customer, index_name: :domain_index
53
- # indexed_by :display_name, in: :global, index_name: :global_domain_index
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.find_by_display_name_globally(name) # Global lookup
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 && respond_to?(:update_all_indexes)
272
- # Update all indexes with current field values
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: Tracking and membership updates are typically done explicitly
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 define a list of fields that
9
- # are safe to dump. The fields can be either symbols or hashes. If a field is
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
- # @safe_dump_fields = [
23
- # :objid,
24
- # :updated,
25
- # :created,
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
- # Internally, all fields are normalized to the hash syntax and stored in
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
- # class MyModel
42
- # include Familia::Features::SafeDump
35
+ # safe_dump_fields :objid, :updated, :created,
36
+ # { active: ->(obj) { obj.active? } }
43
37
  #
44
- # @safe_dump_fields = [
45
- # :id, :name, { active: ->(obj) { obj.active? } }
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 :LOADED, self, base, caller(1..1) if Familia.debug?
47
+ Familia.trace(:LOADED, self, base, caller(1..1)) if Familia.debug?
58
48
  base.extend ClassMethods
59
49
 
60
- # Optionally define safe_dump_fields in the class to make
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
- def set_safe_dump_fields(*fields)
72
- @safe_dump_fields = fields
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
- # `SafeDump.safe_dump_fields` returns only the list
76
- # of symbols in the order they were defined.
77
- def safe_dump_fields
78
- @safe_dump_fields.map do |field|
79
- field.is_a?(Symbol) ? field : field.keys.first
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
- # `SafeDump.safe_dump_field_map` returns the field map
84
- # that is used to dump the fields. The keys are the
85
- # field names and the values are callables that will
86
- # expect to receive the instance object as an argument.
87
- #
88
- # The map is cached on the first call to this method.
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
- return @safe_dump_field_map if @safe_dump_field_map.any?
101
+ @safe_dump_field_map || {}
102
+ end
92
103
 
93
- # Operate directly on the @safe_dump_fields array to
94
- # build the map. This way we'll get the elements defined
95
- # in the hash syntax (i.e. since the safe_dump_fields getter
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
@@ -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 provied check that it's a valid feature
106
+ # If there's a value provided check that it's a valid feature
18
107
  feature_name = feature_name.to_sym
19
- unless Familia::Base.features_available.key?(feature_name)
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 klass
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
- # Get feature options for a specific feature or all features
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
- # @param feature_name [Symbol, nil] The feature name to get options for
241
- # @return [Hash] The options hash for the feature, or empty hash if none
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] ||= {}
@@ -104,39 +104,30 @@ module Familia
104
104
  args = []
105
105
  end
106
106
 
107
- # Initialize object with arguments using one of three strategies:
107
+ # Initialize object with arguments using one of four strategies:
108
108
  #
109
- # 1. **Keyword Arguments** (Recommended): Order-independent field assignment
110
- # Example: Customer.new(name: "John", email: "john@example.com")
111
- # - Robust against field reordering
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. **Positional Arguments** (Legacy): Field assignment by definition order
116
- # Example: Customer.new("john@example.com", "password123")
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. **No Arguments**: Object created with all fields as nil
122
- # - Minimal memory footprint in Redis
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
- # Note: We iterate over self.class.fields (not kwargs) to ensure only
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.any?
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
- Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
135
- # Default values are intentionally NOT set here to:
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