familia 2.0.0.pre6 → 2.0.0.pre8
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 -13
- data/Gemfile +2 -2
- data/Gemfile.lock +3 -3
- data/README.md +35 -0
- data/docs/wiki/Feature-System-Guide.md +36 -20
- data/docs/wiki/Home.md +30 -20
- data/docs/wiki/Relationships-Guide.md +684 -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/connection.rb +3 -3
- data/lib/familia/data_type.rb +7 -4
- data/lib/familia/features/encrypted_fields/concealed_string.rb +21 -23
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
- data/lib/familia/features/external_identifiers.rb +111 -0
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
- data/lib/familia/features/object_identifiers.rb +194 -0
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +437 -0
- data/lib/familia/features/relationships/indexing.rb +369 -0
- data/lib/familia/features/relationships/membership.rb +502 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +615 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +440 -0
- data/lib/familia/features/relationships/tracking.rb +378 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/transient_fields.rb +190 -10
- data/lib/familia/features.rb +18 -14
- data/lib/familia/horreum/core/serialization.rb +2 -5
- data/lib/familia/horreum/subclass/definition.rb +35 -1
- 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 -3
- data/try/core/errors_try.rb +1 -1
- 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 +3 -1
- data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +3 -0
- data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +1 -0
- data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
- data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
- data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
- data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
- data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
- data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
- data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
- data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
- data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
- data/try/features/relationships/categorical_permissions_try.rb +515 -0
- data/try/features/relationships/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships/relationships_performance_try.rb +420 -0
- data/try/features/relationships/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships/relationships_try.rb +237 -0
- data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
- data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +4 -1
- 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 → transient_fields/transient_fields_core_try.rb} +1 -1
- data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
- data/try/helpers/test_helpers.rb +1 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +2 -0
- data/try/horreum/relations_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 +62 -27
- data/docs/wiki/RelatableObjects-Guide.md +0 -563
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/try/features/relatable_objects_try.rb +0 -220
- /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# lib/familia/features/object_identifiers/object_identifier_field_type.rb
|
2
|
+
|
3
|
+
require 'familia/field_type'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Features
|
7
|
+
module ObjectIdentifiers
|
8
|
+
# ObjectIdentifierFieldType - Fields that generate unique object identifiers
|
9
|
+
#
|
10
|
+
# Object identifier fields automatically generate unique identifiers when first
|
11
|
+
# accessed if not already set. The generation strategy is configurable via
|
12
|
+
# feature options. These fields preserve any values set during initialization
|
13
|
+
# to ensure data integrity when loading existing objects from Redis.
|
14
|
+
#
|
15
|
+
# @example Using object identifier fields
|
16
|
+
# class User < Familia::Horreum
|
17
|
+
# feature :object_identifiers, generator: :uuid_v7
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# user = User.new
|
21
|
+
# user.objid # Generates UUID v7 on first access
|
22
|
+
#
|
23
|
+
# # Loading existing object preserves ID
|
24
|
+
# user2 = User.new(objid: "existing-uuid")
|
25
|
+
# user2.objid # Returns "existing-uuid", not regenerated
|
26
|
+
#
|
27
|
+
class ObjectIdentifierFieldType < FieldType
|
28
|
+
# Override getter to provide lazy generation with configured strategy
|
29
|
+
#
|
30
|
+
# Generates the identifier using the configured strategy if not already set.
|
31
|
+
# This preserves any values set during initialization while providing
|
32
|
+
# automatic generation for new objects.
|
33
|
+
#
|
34
|
+
# @param klass [Class] The class to define the method on
|
35
|
+
#
|
36
|
+
def define_getter(klass)
|
37
|
+
field_name = @name
|
38
|
+
method_name = @method_name
|
39
|
+
|
40
|
+
handle_method_conflict(klass, method_name) do
|
41
|
+
klass.define_method method_name do
|
42
|
+
# Check if we already have a value (from initialization or previous generation)
|
43
|
+
existing_value = instance_variable_get(:"@#{field_name}")
|
44
|
+
return existing_value unless existing_value.nil?
|
45
|
+
|
46
|
+
# Generate new identifier using configured strategy
|
47
|
+
generated_id = generate_object_identifier
|
48
|
+
instance_variable_set(:"@#{field_name}", generated_id)
|
49
|
+
generated_id
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Override setter to preserve values during initialization
|
55
|
+
#
|
56
|
+
# This ensures that values passed during object initialization
|
57
|
+
# (e.g., when loading from Redis) are preserved and not overwritten
|
58
|
+
# by the lazy generation logic.
|
59
|
+
#
|
60
|
+
# @param klass [Class] The class to define the method on
|
61
|
+
#
|
62
|
+
def define_setter(klass)
|
63
|
+
field_name = @name
|
64
|
+
method_name = @method_name
|
65
|
+
|
66
|
+
handle_method_conflict(klass, :"#{method_name}=") do
|
67
|
+
klass.define_method :"#{method_name}=" do |value|
|
68
|
+
instance_variable_set(:"@#{field_name}", value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Object identifier fields are persisted to database
|
74
|
+
#
|
75
|
+
# @return [Boolean] true - object identifiers are always persisted
|
76
|
+
#
|
77
|
+
def persistent?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
# Category for object identifier fields
|
82
|
+
#
|
83
|
+
# @return [Symbol] :object_identifier
|
84
|
+
#
|
85
|
+
def category
|
86
|
+
:object_identifier
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# lib/familia/features/object_identifiers.rb
|
2
|
+
|
3
|
+
require_relative 'object_identifiers/object_identifier_field_type'
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Features
|
7
|
+
# ObjectIdentifiers is a feature that provides unique object identifier management
|
8
|
+
# with configurable generation strategies. Object identifiers are crucial for
|
9
|
+
# distinguishing objects in distributed systems and providing stable references.
|
10
|
+
#
|
11
|
+
# Object identifiers are:
|
12
|
+
# - Unique across the system
|
13
|
+
# - Persistent (stored in Redis/Valkey)
|
14
|
+
# - Lazily generated (only when first accessed)
|
15
|
+
# - Configurable (multiple generation strategies available)
|
16
|
+
# - Preserved during initialization (existing IDs never regenerated)
|
17
|
+
#
|
18
|
+
# Generation Strategies:
|
19
|
+
# - :uuid_v7 (default) - UUID version 7 with embedded timestamp for sortability
|
20
|
+
# - :uuid_v4 - UUID version 4 for compatibility with legacy systems
|
21
|
+
# - :hex - High-entropy hexadecimal identifier using SecureIdentifier
|
22
|
+
# - Proc - Custom generation logic provided as a callable
|
23
|
+
#
|
24
|
+
# Example Usage:
|
25
|
+
#
|
26
|
+
# # Default UUID v7 generation
|
27
|
+
# class User < Familia::Horreum
|
28
|
+
# feature :object_identifiers
|
29
|
+
# field :email
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# user = User.new(email: 'user@example.com')
|
33
|
+
# user.objid # => "01234567-89ab-7def-8000-123456789abc" (UUID v7)
|
34
|
+
#
|
35
|
+
# # UUID v4 for legacy compatibility
|
36
|
+
# class LegacyUser < Familia::Horreum
|
37
|
+
# feature :object_identifiers, generator: :uuid_v4
|
38
|
+
# field :email
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# legacy = LegacyUser.new(email: 'legacy@example.com')
|
42
|
+
# legacy.objid # => "f47ac10b-58cc-4372-a567-0e02b2c3d479" (UUID v4)
|
43
|
+
#
|
44
|
+
# # High-entropy hex for security-sensitive applications
|
45
|
+
# class SecureDocument < Familia::Horreum
|
46
|
+
# feature :object_identifiers, generator: :hex
|
47
|
+
# field :title
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# doc = SecureDocument.new(title: 'Classified')
|
51
|
+
# doc.objid # => "a1b2c3d4e5f6..." (256-bit hex)
|
52
|
+
#
|
53
|
+
# # Custom generation strategy
|
54
|
+
# class TimestampedItem < Familia::Horreum
|
55
|
+
# feature :object_identifiers, generator: -> { "item_#{Time.now.to_i}_#{SecureRandom.hex(4)}" }
|
56
|
+
# field :data
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# item = TimestampedItem.new(data: 'test')
|
60
|
+
# item.objid # => "item_1693857600_a1b2c3d4"
|
61
|
+
#
|
62
|
+
# Data Integrity Guarantees:
|
63
|
+
#
|
64
|
+
# The feature preserves object identifiers passed during initialization,
|
65
|
+
# ensuring that existing objects loaded from Redis maintain their IDs:
|
66
|
+
#
|
67
|
+
# # Loading existing object from Redis preserves ID
|
68
|
+
# existing = User.new(objid: 'existing-uuid-value', email: 'existing@example.com')
|
69
|
+
# existing.objid # => "existing-uuid-value" (preserved, not regenerated)
|
70
|
+
#
|
71
|
+
# Performance Characteristics:
|
72
|
+
#
|
73
|
+
# - Lazy Generation: IDs generated only when first accessed
|
74
|
+
# - Thread-Safe: Generator strategy configured once during initialization
|
75
|
+
# - Memory Efficient: No unnecessary ID generation for unused objects
|
76
|
+
# - Redis Efficient: Only persists non-nil values to conserve memory
|
77
|
+
#
|
78
|
+
# Security Considerations:
|
79
|
+
#
|
80
|
+
# - UUID v7 includes timestamp information (may leak timing data)
|
81
|
+
# - UUID v4 provides strong randomness without timing correlation
|
82
|
+
# - Hex generator provides maximum entropy (256 bits) for security-critical use cases
|
83
|
+
# - Custom generators allow domain-specific security requirements
|
84
|
+
#
|
85
|
+
module ObjectIdentifiers
|
86
|
+
DEFAULT_GENERATOR = :uuid_v7
|
87
|
+
|
88
|
+
def self.included(base)
|
89
|
+
Familia.trace :LOADED, self, base, caller(1..1) if Familia.debug?
|
90
|
+
base.extend ClassMethods
|
91
|
+
|
92
|
+
# Ensure default generator is set in feature options
|
93
|
+
base.add_feature_options(:object_identifiers, generator: DEFAULT_GENERATOR)
|
94
|
+
|
95
|
+
# Register the objid field using our custom field type
|
96
|
+
base.register_field_type(
|
97
|
+
ObjectIdentifiers::ObjectIdentifierFieldType.new(:objid, as: :objid, fast_method: false)
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
module ClassMethods
|
102
|
+
# Generate a new object identifier using the configured strategy
|
103
|
+
#
|
104
|
+
# @return [String] A new unique identifier
|
105
|
+
#
|
106
|
+
def generate_objid
|
107
|
+
options = feature_options(:object_identifiers)
|
108
|
+
generator = options[:generator] || DEFAULT_GENERATOR
|
109
|
+
|
110
|
+
case generator
|
111
|
+
when :uuid_v7
|
112
|
+
SecureRandom.uuid_v7
|
113
|
+
when :uuid_v4
|
114
|
+
SecureRandom.uuid_v4
|
115
|
+
when :hex
|
116
|
+
Familia.generate_hex_id
|
117
|
+
when Proc
|
118
|
+
generator.call
|
119
|
+
else
|
120
|
+
unless generator.respond_to?(:call)
|
121
|
+
raise Familia::Problem, "Invalid object identifier generator: #{generator.inspect}"
|
122
|
+
end
|
123
|
+
|
124
|
+
generator.call
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Find an object by its object identifier
|
130
|
+
#
|
131
|
+
# @param objid [String] The object identifier to search for
|
132
|
+
# @return [Object, nil] The object if found, nil otherwise
|
133
|
+
#
|
134
|
+
def find_by_objid(objid)
|
135
|
+
return nil if objid.to_s.empty?
|
136
|
+
|
137
|
+
if Familia.debug?
|
138
|
+
reference = caller(1..1).first
|
139
|
+
Familia.trace :FIND_BY_OBJID, Familia.dbclient, objid, reference
|
140
|
+
end
|
141
|
+
|
142
|
+
# Use the object identifier as the key for lookup
|
143
|
+
# This is a simple stub implementation - would need more sophisticated
|
144
|
+
# search logic in a real application
|
145
|
+
find_by_id(objid)
|
146
|
+
rescue Familia::NotFound
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Instance method for generating object identifier using configured strategy
|
152
|
+
#
|
153
|
+
# This method is called by the ObjectIdentifierFieldType when lazy generation
|
154
|
+
# is needed. It uses the class-level generator configuration to create new IDs.
|
155
|
+
#
|
156
|
+
# @return [String] A newly generated unique identifier
|
157
|
+
# @private
|
158
|
+
#
|
159
|
+
def generate_object_identifier
|
160
|
+
self.class.generate_objid
|
161
|
+
end
|
162
|
+
|
163
|
+
# Alias for objid for consistency with naming conventions
|
164
|
+
#
|
165
|
+
# @return [String] The object identifier
|
166
|
+
#
|
167
|
+
def object_identifier
|
168
|
+
objid
|
169
|
+
end
|
170
|
+
|
171
|
+
# Initialize object identifier configuration
|
172
|
+
#
|
173
|
+
# Called during object initialization to set up the ID generation strategy.
|
174
|
+
# This hook is called AFTER field initialization, ensuring that any objid
|
175
|
+
# values passed during construction are preserved.
|
176
|
+
#
|
177
|
+
def init
|
178
|
+
super if defined?(super)
|
179
|
+
|
180
|
+
# The generator strategy is configured at the class level via feature options.
|
181
|
+
# We don't need to store it per-instance since it's consistent for the class.
|
182
|
+
# The actual generation happens lazily in the getter when needed.
|
183
|
+
|
184
|
+
return unless Familia.debug?
|
185
|
+
|
186
|
+
options = self.class.feature_options(:object_identifiers)
|
187
|
+
generator = options[:generator] || DEFAULT_GENERATOR
|
188
|
+
Familia.trace :OBJID_INIT, dbclient, "Generator strategy: #{generator}", caller(1..1)
|
189
|
+
end
|
190
|
+
|
191
|
+
Familia::Base.add_feature self, :object_identifiers, depends_on: []
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|