familia 2.0.0.pre5 → 2.0.0.pre7
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/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,784 @@
|
|
1
|
+
# Field System Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
Familia's Field System provides a flexible, extensible architecture for defining and managing object attributes with customizable behavior, conflict resolution, and serialization. The system uses a FieldType-based architecture that separates field definition from implementation, enabling custom field behaviors and advanced features.
|
6
|
+
|
7
|
+
## Core Architecture
|
8
|
+
|
9
|
+
### FieldType System
|
10
|
+
|
11
|
+
The Field System is built around the `FieldType` class hierarchy:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
FieldType # Base class for all field types
|
15
|
+
├── TransientFieldType # Non-persistent fields (memory only)
|
16
|
+
├── EncryptedFieldType # Encrypted storage fields
|
17
|
+
└── Custom field types # User-defined field behaviors
|
18
|
+
```
|
19
|
+
|
20
|
+
### Field Definition Flow
|
21
|
+
|
22
|
+
1. **Field Declaration**: `field :name, options...`
|
23
|
+
2. **FieldType Creation**: Appropriate FieldType instance created
|
24
|
+
3. **Registration**: FieldType registered with the class
|
25
|
+
4. **Method Installation**: Getter, setter, and fast methods defined
|
26
|
+
5. **Runtime Usage**: Methods available on instances
|
27
|
+
|
28
|
+
## Basic Usage
|
29
|
+
|
30
|
+
### Simple Field Definition
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class Customer < Familia::Horreum
|
34
|
+
# Basic field with default settings
|
35
|
+
field :name
|
36
|
+
|
37
|
+
# Field with custom method name
|
38
|
+
field :email_address, as: :email
|
39
|
+
|
40
|
+
# Field without accessor methods
|
41
|
+
field :internal_data, as: false
|
42
|
+
|
43
|
+
# Field without fast writer method
|
44
|
+
field :readonly_data, fast_method: false
|
45
|
+
end
|
46
|
+
|
47
|
+
customer = Customer.new
|
48
|
+
customer.name = "Acme Corp" # Standard setter
|
49
|
+
customer.email = "admin@acme.com" # Custom method name
|
50
|
+
customer.name!("Updated Corp") # Fast writer (immediate DB persistence)
|
51
|
+
```
|
52
|
+
|
53
|
+
### Field Categories
|
54
|
+
|
55
|
+
Fields can be categorized for special processing by features:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class Document < Familia::Horreum
|
59
|
+
field :title # Regular field
|
60
|
+
field :content, category: :encrypted # Will be processed by encrypted_fields feature
|
61
|
+
field :api_key, category: :transient # Non-persistent field
|
62
|
+
field :tags, category: :indexed # Custom category for indexing
|
63
|
+
field :metadata, category: :json # Custom JSON serialization
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
### Method Conflict Resolution
|
68
|
+
|
69
|
+
The Field System provides multiple strategies for handling method name conflicts:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class Customer < Familia::Horreum
|
73
|
+
# Raise error if method exists (default)
|
74
|
+
field :status, on_conflict: :raise
|
75
|
+
|
76
|
+
# Skip field definition if method exists
|
77
|
+
field :type, on_conflict: :skip
|
78
|
+
|
79
|
+
# Warn but proceed with definition
|
80
|
+
field :class, on_conflict: :warn
|
81
|
+
|
82
|
+
# Silently overwrite existing method
|
83
|
+
field :id, on_conflict: :overwrite
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
## Advanced Field Types
|
88
|
+
|
89
|
+
### Creating Custom Field Types
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Custom field type for timestamps
|
93
|
+
class TimestampFieldType < Familia::FieldType
|
94
|
+
def category
|
95
|
+
:timestamp
|
96
|
+
end
|
97
|
+
|
98
|
+
def define_setter(klass)
|
99
|
+
field_name = @name
|
100
|
+
method_name = @method_name
|
101
|
+
|
102
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
103
|
+
klass.define_method :"#{method_name}=" do |value|
|
104
|
+
# Convert various formats to Unix timestamp
|
105
|
+
timestamp = case value
|
106
|
+
when Time then value.to_i
|
107
|
+
when String then Time.parse(value).to_i
|
108
|
+
when Numeric then value.to_i
|
109
|
+
else raise ArgumentError, "Invalid timestamp: #{value}"
|
110
|
+
end
|
111
|
+
instance_variable_set(:"@#{field_name}", timestamp)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def define_getter(klass)
|
117
|
+
field_name = @name
|
118
|
+
method_name = @method_name
|
119
|
+
|
120
|
+
handle_method_conflict(klass, method_name) do
|
121
|
+
klass.define_method method_name do
|
122
|
+
timestamp = instance_variable_get(:"@#{field_name}")
|
123
|
+
timestamp ? Time.at(timestamp) : nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def serialize(value, _record = nil)
|
129
|
+
value.respond_to?(:to_i) ? value.to_i : value
|
130
|
+
end
|
131
|
+
|
132
|
+
def deserialize(value, _record = nil)
|
133
|
+
value ? Time.at(value.to_i) : nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Register and use the custom field type
|
138
|
+
class Event < Familia::Horreum
|
139
|
+
def self.timestamp_field(name, **options)
|
140
|
+
field_type = TimestampFieldType.new(name, **options)
|
141
|
+
register_field_type(field_type)
|
142
|
+
end
|
143
|
+
|
144
|
+
identifier_field :event_id
|
145
|
+
field :event_id, :name, :description
|
146
|
+
timestamp_field :created_at
|
147
|
+
timestamp_field :updated_at
|
148
|
+
end
|
149
|
+
|
150
|
+
# Usage
|
151
|
+
event = Event.new(event_id: 'evt_123')
|
152
|
+
event.created_at = "2023-06-15 14:30:00" # String input
|
153
|
+
puts event.created_at.class # => Time
|
154
|
+
puts event.created_at # => 2023-06-15 14:30:00 UTC
|
155
|
+
```
|
156
|
+
|
157
|
+
### JSON Field Type
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
class JsonFieldType < Familia::FieldType
|
161
|
+
def category
|
162
|
+
:json
|
163
|
+
end
|
164
|
+
|
165
|
+
def define_setter(klass)
|
166
|
+
field_name = @name
|
167
|
+
method_name = @method_name
|
168
|
+
|
169
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
170
|
+
klass.define_method :"#{method_name}=" do |value|
|
171
|
+
# Store as parsed JSON for manipulation
|
172
|
+
parsed_value = case value
|
173
|
+
when String then JSON.parse(value)
|
174
|
+
when Hash, Array then value
|
175
|
+
else raise ArgumentError, "Value must be JSON string, Hash, or Array"
|
176
|
+
end
|
177
|
+
instance_variable_set(:"@#{field_name}", parsed_value)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def serialize(value, _record = nil)
|
183
|
+
value.to_json if value
|
184
|
+
end
|
185
|
+
|
186
|
+
def deserialize(value, _record = nil)
|
187
|
+
value ? JSON.parse(value) : nil
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class Configuration < Familia::Horreum
|
192
|
+
def self.json_field(name, **options)
|
193
|
+
field_type = JsonFieldType.new(name, **options)
|
194
|
+
register_field_type(field_type)
|
195
|
+
end
|
196
|
+
|
197
|
+
identifier_field :config_id
|
198
|
+
field :config_id, :name
|
199
|
+
json_field :settings
|
200
|
+
json_field :metadata
|
201
|
+
end
|
202
|
+
|
203
|
+
# Usage
|
204
|
+
config = Configuration.new(config_id: 'app_config')
|
205
|
+
config.settings = { theme: 'dark', notifications: true }
|
206
|
+
config.settings['api_timeout'] = 30
|
207
|
+
|
208
|
+
# Automatically serialized to JSON in database
|
209
|
+
config.save
|
210
|
+
# Database stores: {"theme":"dark","notifications":true,"api_timeout":30}
|
211
|
+
```
|
212
|
+
|
213
|
+
### Enum Field Type
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
class EnumFieldType < Familia::FieldType
|
217
|
+
def initialize(name, values:, **options)
|
218
|
+
super(name, **options)
|
219
|
+
@valid_values = values.map(&:to_s).to_set
|
220
|
+
@default_value = values.first
|
221
|
+
end
|
222
|
+
|
223
|
+
def category
|
224
|
+
:enum
|
225
|
+
end
|
226
|
+
|
227
|
+
def define_setter(klass)
|
228
|
+
field_name = @name
|
229
|
+
method_name = @method_name
|
230
|
+
valid_values = @valid_values
|
231
|
+
|
232
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
233
|
+
klass.define_method :"#{method_name}=" do |value|
|
234
|
+
value_str = value.to_s
|
235
|
+
unless valid_values.include?(value_str)
|
236
|
+
raise ArgumentError, "Invalid #{field_name}: #{value}. Valid values: #{valid_values.to_a.join(', ')}"
|
237
|
+
end
|
238
|
+
instance_variable_set(:"@#{field_name}", value_str)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Add predicate methods for each enum value
|
244
|
+
def install(klass)
|
245
|
+
super(klass)
|
246
|
+
|
247
|
+
@valid_values.each do |value|
|
248
|
+
predicate_method = :"#{@method_name}_#{value}?"
|
249
|
+
field_name = @name
|
250
|
+
|
251
|
+
klass.define_method predicate_method do
|
252
|
+
instance_variable_get(:"@#{field_name}") == value
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
class Order < Familia::Horreum
|
259
|
+
def self.enum_field(name, values:, **options)
|
260
|
+
field_type = EnumFieldType.new(name, values: values, **options)
|
261
|
+
register_field_type(field_type)
|
262
|
+
end
|
263
|
+
|
264
|
+
identifier_field :order_id
|
265
|
+
field :order_id, :customer_id
|
266
|
+
enum_field :status, values: [:pending, :processing, :shipped, :delivered, :cancelled]
|
267
|
+
enum_field :priority, values: [:low, :normal, :high, :urgent]
|
268
|
+
end
|
269
|
+
|
270
|
+
# Usage
|
271
|
+
order = Order.new(order_id: 'ord_123')
|
272
|
+
order.status = :pending
|
273
|
+
order.priority = 'high'
|
274
|
+
|
275
|
+
# Predicate methods automatically available
|
276
|
+
order.status_pending? # => true
|
277
|
+
order.status_shipped? # => false
|
278
|
+
order.priority_high? # => true
|
279
|
+
order.priority_urgent? # => false
|
280
|
+
```
|
281
|
+
|
282
|
+
## Field Metadata and Introspection
|
283
|
+
|
284
|
+
### Accessing Field Information
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
class Product < Familia::Horreum
|
288
|
+
field :name, category: :searchable
|
289
|
+
field :price, category: :numeric
|
290
|
+
field :description, category: :text
|
291
|
+
field :secret_key, category: :encrypted
|
292
|
+
transient_field :temp_data
|
293
|
+
end
|
294
|
+
|
295
|
+
# Get all field names
|
296
|
+
Product.fields
|
297
|
+
# => [:name, :price, :description, :secret_key, :temp_data]
|
298
|
+
|
299
|
+
# Get field types registry
|
300
|
+
Product.field_types
|
301
|
+
# => { name: #<FieldType...>, price: #<FieldType...>, ... }
|
302
|
+
|
303
|
+
# Get fields by category
|
304
|
+
Product.fields.select { |f| Product.field_types[f].category == :searchable }
|
305
|
+
# => [:name]
|
306
|
+
|
307
|
+
# Get persistent vs transient fields
|
308
|
+
Product.persistent_fields # => [:name, :price, :description, :secret_key]
|
309
|
+
Product.transient_fields # => [:temp_data]
|
310
|
+
|
311
|
+
# Field method mapping (for backward compatibility)
|
312
|
+
Product.field_method_map
|
313
|
+
# => { name: :name, price: :price, secret_key: :secret_key, temp_data: :temp_data }
|
314
|
+
```
|
315
|
+
|
316
|
+
### Field Categories for Feature Processing
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
# Features can process fields by category
|
320
|
+
module SearchableFieldsFeature
|
321
|
+
def self.included(base)
|
322
|
+
base.extend ClassMethods
|
323
|
+
|
324
|
+
# Process all searchable fields
|
325
|
+
searchable_fields = base.fields.select do |field|
|
326
|
+
base.field_types[field].category == :searchable
|
327
|
+
end
|
328
|
+
|
329
|
+
searchable_fields.each do |field|
|
330
|
+
create_search_index_for(base, field)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
module ClassMethods
|
335
|
+
def search_by_field(field_name, query)
|
336
|
+
# Implementation for field-specific search
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
private
|
341
|
+
|
342
|
+
def self.create_search_index_for(klass, field_name)
|
343
|
+
# Create search index methods
|
344
|
+
klass.define_singleton_method :"search_by_#{field_name}" do |query|
|
345
|
+
# Search implementation
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
class Product < Familia::Horreum
|
351
|
+
feature :searchable_fields # Processes all :searchable category fields
|
352
|
+
|
353
|
+
field :name, category: :searchable
|
354
|
+
field :description, category: :searchable
|
355
|
+
field :internal_id, category: :system
|
356
|
+
end
|
357
|
+
|
358
|
+
# Auto-generated search methods available
|
359
|
+
Product.search_by_name("laptop")
|
360
|
+
Product.search_by_description("gaming")
|
361
|
+
```
|
362
|
+
|
363
|
+
## Fast Methods and Database Operations
|
364
|
+
|
365
|
+
### Fast Method Behavior
|
366
|
+
|
367
|
+
Fast methods provide immediate database persistence without affecting other object state:
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
class UserProfile < Familia::Horreum
|
371
|
+
identifier_field :user_id
|
372
|
+
field :user_id, :name, :email, :last_login_at
|
373
|
+
end
|
374
|
+
|
375
|
+
profile = UserProfile.new(user_id: 'user_123')
|
376
|
+
profile.save
|
377
|
+
|
378
|
+
# Regular setter: updates instance variable only
|
379
|
+
profile.last_login_at = Time.now # Not yet in database
|
380
|
+
|
381
|
+
# Fast method: immediate database write
|
382
|
+
profile.last_login_at!(Time.now) # Written to database immediately
|
383
|
+
|
384
|
+
# Reading from database
|
385
|
+
profile.last_login_at # => reads from instance variable
|
386
|
+
profile.last_login_at! # => reads directly from database
|
387
|
+
```
|
388
|
+
|
389
|
+
### Custom Fast Method Behavior
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
class AuditedFieldType < Familia::FieldType
|
393
|
+
def define_fast_writer(klass)
|
394
|
+
return unless @fast_method_name
|
395
|
+
|
396
|
+
field_name = @name
|
397
|
+
method_name = @method_name
|
398
|
+
fast_method_name = @fast_method_name
|
399
|
+
|
400
|
+
handle_method_conflict(klass, fast_method_name) do
|
401
|
+
klass.define_method fast_method_name do |*args|
|
402
|
+
if args.empty?
|
403
|
+
# Read from database
|
404
|
+
hget(field_name)
|
405
|
+
else
|
406
|
+
# Write to database with audit trail
|
407
|
+
value = args.first
|
408
|
+
old_value = hget(field_name)
|
409
|
+
|
410
|
+
# Update the field
|
411
|
+
prepared = serialize_value(value)
|
412
|
+
send(:"#{method_name}=", value) if method_name
|
413
|
+
result = hset(field_name, prepared)
|
414
|
+
|
415
|
+
# Create audit entry
|
416
|
+
audit_entry = {
|
417
|
+
field: field_name,
|
418
|
+
old_value: old_value,
|
419
|
+
new_value: value,
|
420
|
+
changed_at: Time.now.to_f,
|
421
|
+
changed_by: Thread.current[:current_user]&.id
|
422
|
+
}
|
423
|
+
|
424
|
+
# Store audit trail
|
425
|
+
audit_key = "#{dbkey}:audit"
|
426
|
+
Familia.dbclient.lpush(audit_key, audit_entry.to_json)
|
427
|
+
Familia.dbclient.ltrim(audit_key, 0, 99) # Keep last 100 changes
|
428
|
+
|
429
|
+
result
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
class AuditedDocument < Familia::Horreum
|
437
|
+
def self.audited_field(name, **options)
|
438
|
+
field_type = AuditedFieldType.new(name, **options)
|
439
|
+
register_field_type(field_type)
|
440
|
+
end
|
441
|
+
|
442
|
+
identifier_field :doc_id
|
443
|
+
field :doc_id, :title
|
444
|
+
audited_field :content
|
445
|
+
audited_field :status
|
446
|
+
end
|
447
|
+
|
448
|
+
# Usage creates audit trail
|
449
|
+
doc = AuditedDocument.new(doc_id: 'doc_123')
|
450
|
+
doc.save
|
451
|
+
|
452
|
+
Thread.current[:current_user] = OpenStruct.new(id: 'user_456')
|
453
|
+
doc.content!("Initial content") # Audited change
|
454
|
+
doc.status!("draft") # Audited change
|
455
|
+
|
456
|
+
# View audit trail
|
457
|
+
audit_key = "#{doc.dbkey}:audit"
|
458
|
+
audit_entries = Familia.dbclient.lrange(audit_key, 0, -1)
|
459
|
+
audit_entries.map { |entry| JSON.parse(entry) }
|
460
|
+
```
|
461
|
+
|
462
|
+
## Integration Patterns
|
463
|
+
|
464
|
+
### Rails Integration
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
# app/models/concerns/familia_fields.rb
|
468
|
+
module FamiliaFields
|
469
|
+
extend ActiveSupport::Concern
|
470
|
+
|
471
|
+
class_methods do
|
472
|
+
# Rails-style field definitions
|
473
|
+
def string_field(name, **options)
|
474
|
+
field(name, **options)
|
475
|
+
end
|
476
|
+
|
477
|
+
def integer_field(name, **options)
|
478
|
+
field_type = Class.new(Familia::FieldType) do
|
479
|
+
def serialize(value, _record = nil)
|
480
|
+
value.to_i if value
|
481
|
+
end
|
482
|
+
|
483
|
+
def deserialize(value, _record = nil)
|
484
|
+
value.to_i if value
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
register_field_type(field_type.new(name, **options))
|
489
|
+
end
|
490
|
+
|
491
|
+
def boolean_field(name, **options)
|
492
|
+
field_type = Class.new(Familia::FieldType) do
|
493
|
+
def serialize(value, _record = nil)
|
494
|
+
!!value
|
495
|
+
end
|
496
|
+
|
497
|
+
def deserialize(value, _record = nil)
|
498
|
+
value == true || value == 'true' || value == '1'
|
499
|
+
end
|
500
|
+
|
501
|
+
def define_getter(klass)
|
502
|
+
super(klass)
|
503
|
+
|
504
|
+
# Add predicate method
|
505
|
+
predicate_method = :"#{@method_name}?"
|
506
|
+
field_name = @name
|
507
|
+
|
508
|
+
klass.define_method predicate_method do
|
509
|
+
!!instance_variable_get(:"@#{field_name}")
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
register_field_type(field_type.new(name, **options))
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
class User < Familia::Horreum
|
520
|
+
include FamiliaFields
|
521
|
+
|
522
|
+
identifier_field :user_id
|
523
|
+
string_field :user_id, :email, :name
|
524
|
+
integer_field :age, :login_count
|
525
|
+
boolean_field :active, :verified
|
526
|
+
end
|
527
|
+
|
528
|
+
user = User.new(user_id: 'user_123')
|
529
|
+
user.age = "25" # Automatically converted to integer
|
530
|
+
user.active = "true" # Automatically converted to boolean
|
531
|
+
user.verified? # => false (predicate method)
|
532
|
+
```
|
533
|
+
|
534
|
+
### Validation Integration
|
535
|
+
|
536
|
+
```ruby
|
537
|
+
class ValidatedFieldType < Familia::FieldType
|
538
|
+
def initialize(name, validations: {}, **options)
|
539
|
+
super(name, **options)
|
540
|
+
@validations = validations
|
541
|
+
end
|
542
|
+
|
543
|
+
def define_setter(klass)
|
544
|
+
field_name = @name
|
545
|
+
method_name = @method_name
|
546
|
+
validations = @validations
|
547
|
+
|
548
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
549
|
+
klass.define_method :"#{method_name}=" do |value|
|
550
|
+
# Run validations
|
551
|
+
validations.each do |validator, constraint|
|
552
|
+
case validator
|
553
|
+
when :presence
|
554
|
+
if constraint && (value.nil? || value.to_s.strip.empty?)
|
555
|
+
raise ArgumentError, "#{field_name} cannot be blank"
|
556
|
+
end
|
557
|
+
when :length
|
558
|
+
if constraint.is_a?(Hash) && constraint[:minimum]
|
559
|
+
if value.to_s.length < constraint[:minimum]
|
560
|
+
raise ArgumentError, "#{field_name} is too short (minimum #{constraint[:minimum]} characters)"
|
561
|
+
end
|
562
|
+
end
|
563
|
+
when :format
|
564
|
+
if constraint.is_a?(Regexp) && !value.to_s.match?(constraint)
|
565
|
+
raise ArgumentError, "#{field_name} format is invalid"
|
566
|
+
end
|
567
|
+
when :inclusion
|
568
|
+
if constraint.is_a?(Array) && !constraint.include?(value)
|
569
|
+
raise ArgumentError, "#{field_name} must be one of: #{constraint.join(', ')}"
|
570
|
+
end
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
instance_variable_set(:"@#{field_name}", value)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
class User < Familia::Horreum
|
581
|
+
def self.validated_field(name, validations: {}, **options)
|
582
|
+
field_type = ValidatedFieldType.new(name, validations: validations, **options)
|
583
|
+
register_field_type(field_type)
|
584
|
+
end
|
585
|
+
|
586
|
+
identifier_field :user_id
|
587
|
+
validated_field :user_id, validations: { presence: true }
|
588
|
+
validated_field :email, validations: {
|
589
|
+
presence: true,
|
590
|
+
format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
591
|
+
}
|
592
|
+
validated_field :status, validations: {
|
593
|
+
inclusion: %w[active inactive suspended]
|
594
|
+
}
|
595
|
+
validated_field :name, validations: {
|
596
|
+
presence: true,
|
597
|
+
length: { minimum: 2 }
|
598
|
+
}
|
599
|
+
end
|
600
|
+
|
601
|
+
# Usage with validation
|
602
|
+
user = User.new
|
603
|
+
user.email = "invalid-email" # Raises ArgumentError
|
604
|
+
user.status = "unknown" # Raises ArgumentError
|
605
|
+
user.name = "A" # Raises ArgumentError (too short)
|
606
|
+
```
|
607
|
+
|
608
|
+
## Performance Considerations
|
609
|
+
|
610
|
+
### Efficient Field Operations
|
611
|
+
|
612
|
+
```ruby
|
613
|
+
class OptimizedFieldAccess < Familia::Horreum
|
614
|
+
# Cache field type lookups
|
615
|
+
def self.field_type_for(field_name)
|
616
|
+
@field_type_cache ||= {}
|
617
|
+
@field_type_cache[field_name] ||= field_types[field_name]
|
618
|
+
end
|
619
|
+
|
620
|
+
# Batch field updates
|
621
|
+
def batch_update(field_values)
|
622
|
+
# Update instance variables
|
623
|
+
field_values.each do |field, value|
|
624
|
+
setter_method = :"#{field}="
|
625
|
+
send(setter_method, value) if respond_to?(setter_method)
|
626
|
+
end
|
627
|
+
|
628
|
+
# Single database call for persistence
|
629
|
+
serialized_values = field_values.transform_values do |value|
|
630
|
+
serialize_value(value)
|
631
|
+
end
|
632
|
+
|
633
|
+
hmset(serialized_values)
|
634
|
+
end
|
635
|
+
|
636
|
+
# Lazy field loading
|
637
|
+
def lazy_load_field(field_name)
|
638
|
+
return instance_variable_get(:"@#{field_name}") if instance_variable_defined?(:"@#{field_name}")
|
639
|
+
|
640
|
+
value = hget(field_name)
|
641
|
+
field_type = self.class.field_type_for(field_name)
|
642
|
+
deserialized = field_type&.deserialize(value, self) || value
|
643
|
+
|
644
|
+
instance_variable_set(:"@#{field_name}", deserialized)
|
645
|
+
deserialized
|
646
|
+
end
|
647
|
+
end
|
648
|
+
```
|
649
|
+
|
650
|
+
### Memory-Efficient Field Storage
|
651
|
+
|
652
|
+
```ruby
|
653
|
+
class CompactFieldType < Familia::FieldType
|
654
|
+
def serialize(value, _record = nil)
|
655
|
+
case value
|
656
|
+
when String
|
657
|
+
# Compress strings longer than 100 characters
|
658
|
+
if value.length > 100
|
659
|
+
Base64.encode64(Zlib::Deflate.deflate(value))
|
660
|
+
else
|
661
|
+
value
|
662
|
+
end
|
663
|
+
else
|
664
|
+
value
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
def deserialize(value, _record = nil)
|
669
|
+
return value unless value.is_a?(String)
|
670
|
+
|
671
|
+
# Check if it's base64 encoded compressed data
|
672
|
+
if value.length > 100 && value.match?(/\A[A-Za-z0-9+\/]*={0,2}\z/)
|
673
|
+
begin
|
674
|
+
Zlib::Inflate.inflate(Base64.decode64(value))
|
675
|
+
rescue
|
676
|
+
value # Return as-is if decompression fails
|
677
|
+
end
|
678
|
+
else
|
679
|
+
value
|
680
|
+
end
|
681
|
+
end
|
682
|
+
end
|
683
|
+
```
|
684
|
+
|
685
|
+
## Testing Field Types
|
686
|
+
|
687
|
+
### RSpec Testing
|
688
|
+
|
689
|
+
```ruby
|
690
|
+
RSpec.describe TimestampFieldType do
|
691
|
+
let(:field_type) { described_class.new(:created_at) }
|
692
|
+
let(:test_class) do
|
693
|
+
Class.new(Familia::Horreum) do
|
694
|
+
def self.name; 'TestClass'; end
|
695
|
+
end
|
696
|
+
end
|
697
|
+
|
698
|
+
before do
|
699
|
+
field_type.install(test_class)
|
700
|
+
end
|
701
|
+
|
702
|
+
it "converts various time formats" do
|
703
|
+
instance = test_class.new
|
704
|
+
|
705
|
+
instance.created_at = "2023-06-15 14:30:00"
|
706
|
+
expect(instance.created_at).to be_a(Time)
|
707
|
+
|
708
|
+
instance.created_at = Time.now
|
709
|
+
expect(instance.created_at).to be_a(Time)
|
710
|
+
|
711
|
+
instance.created_at = Time.now.to_i
|
712
|
+
expect(instance.created_at).to be_a(Time)
|
713
|
+
end
|
714
|
+
|
715
|
+
it "serializes to integer" do
|
716
|
+
time_value = Time.now
|
717
|
+
serialized = field_type.serialize(time_value)
|
718
|
+
expect(serialized).to be_a(Integer)
|
719
|
+
expect(serialized).to eq(time_value.to_i)
|
720
|
+
end
|
721
|
+
|
722
|
+
it "deserializes from integer" do
|
723
|
+
timestamp = Time.now.to_i
|
724
|
+
deserialized = field_type.deserialize(timestamp)
|
725
|
+
expect(deserialized).to be_a(Time)
|
726
|
+
expect(deserialized.to_i).to eq(timestamp)
|
727
|
+
end
|
728
|
+
end
|
729
|
+
```
|
730
|
+
|
731
|
+
## Best Practices
|
732
|
+
|
733
|
+
### 1. Choose Appropriate Field Types
|
734
|
+
|
735
|
+
```ruby
|
736
|
+
# Use built-in field types when possible
|
737
|
+
class User < Familia::Horreum
|
738
|
+
field :name # Simple string field
|
739
|
+
field :metadata, category: :json # For complex data
|
740
|
+
transient_field :temp_token # For runtime-only data
|
741
|
+
encrypted_field :api_key # For sensitive data
|
742
|
+
end
|
743
|
+
|
744
|
+
# Create custom types for specialized behavior
|
745
|
+
class GeoLocation < Familia::Horreum
|
746
|
+
coordinate_field :latitude # Custom validation and formatting
|
747
|
+
coordinate_field :longitude
|
748
|
+
end
|
749
|
+
```
|
750
|
+
|
751
|
+
### 2. Handle Method Conflicts Gracefully
|
752
|
+
|
753
|
+
```ruby
|
754
|
+
class SafeFieldDefinition < Familia::Horreum
|
755
|
+
# Check for conflicts before defining fields
|
756
|
+
def self.safe_field(name, **options)
|
757
|
+
if method_defined?(name) || method_defined?(:"#{name}=")
|
758
|
+
Rails.logger.warn "Method conflict for field #{name}, using alternative name"
|
759
|
+
options[:as] = :"#{name}_value"
|
760
|
+
end
|
761
|
+
|
762
|
+
field(name, **options)
|
763
|
+
end
|
764
|
+
end
|
765
|
+
```
|
766
|
+
|
767
|
+
### 3. Optimize for Common Use Cases
|
768
|
+
|
769
|
+
```ruby
|
770
|
+
# Provide convenience methods for common patterns
|
771
|
+
class BaseModel < Familia::Horreum
|
772
|
+
def self.timestamps
|
773
|
+
timestamp_field :created_at, as: :created_at
|
774
|
+
timestamp_field :updated_at, as: :updated_at
|
775
|
+
end
|
776
|
+
|
777
|
+
def self.soft_delete
|
778
|
+
boolean_field :deleted, as: :deleted
|
779
|
+
timestamp_field :deleted_at, as: :deleted_at
|
780
|
+
end
|
781
|
+
end
|
782
|
+
```
|
783
|
+
|
784
|
+
The Field System provides a powerful foundation for defining flexible, extensible object attributes with customizable behavior, validation, and serialization capabilities.
|