familia 2.0.0.pre5 → 2.0.0.pre6
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/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- 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 +600 -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 +72 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/RelatableObjects-Guide.md +563 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/lib/familia/base.rb +1 -1
- 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 +2 -2
- 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 +295 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/expiration.rb +1 -1
- data/lib/familia/features/quantization.rb +1 -1
- 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 +1 -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} +44 -28
- 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/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/encryption/encryption_core_try.rb +3 -3
- 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 +250 -0
- data/try/features/encryption_fields/context_isolation_try.rb +29 -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/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +25 -0
- data/try/horreum/enhanced_conflict_handling_try.rb +1 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +1 -1
- 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
- metadata +51 -10
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/horreum/serialization.rb +0 -473
@@ -1,29 +1,38 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/subclass/definition.rb
|
2
2
|
|
3
3
|
require_relative 'related_fields_management'
|
4
|
+
require_relative '../shared/settings'
|
4
5
|
|
5
6
|
module Familia
|
6
|
-
VALID_STRATEGIES = %i[raise skip warn overwrite].freeze
|
7
|
+
VALID_STRATEGIES = %i[raise skip ignore warn overwrite].freeze
|
7
8
|
|
8
9
|
# Familia::Horreum
|
9
10
|
#
|
10
11
|
class Horreum
|
11
12
|
# Class-level instance variables
|
12
13
|
# These are set up as nil initially and populated later
|
13
|
-
|
14
|
-
|
15
|
-
@
|
14
|
+
#
|
15
|
+
# Connection and database settings
|
16
|
+
@dbclient = nil
|
16
17
|
@logical_database = nil
|
17
18
|
@uri = nil
|
18
|
-
|
19
|
+
|
20
|
+
# Database Key generation settings
|
19
21
|
@prefix = nil
|
20
|
-
@
|
21
|
-
@
|
22
|
-
|
22
|
+
@identifier_field = nil
|
23
|
+
@suffix = nil
|
24
|
+
|
25
|
+
# Fields and relationships
|
26
|
+
@fields = nil
|
27
|
+
@class_related_fields = nil
|
28
|
+
@related_fields = nil
|
29
|
+
@default_expiration = nil
|
30
|
+
|
31
|
+
# Serialization settings
|
23
32
|
@dump_method = nil
|
24
33
|
@load_method = nil
|
25
34
|
|
26
|
-
# DefinitionMethods: Provides class-level functionality for Horreum
|
35
|
+
# DefinitionMethods: Provides class-level functionality for Horreum subclasses
|
27
36
|
#
|
28
37
|
# This module is extended into classes that include Familia::Horreum,
|
29
38
|
# providing methods for Database operations and object management.
|
@@ -35,7 +44,7 @@ module Familia
|
|
35
44
|
#
|
36
45
|
module DefinitionMethods
|
37
46
|
include Familia::Settings
|
38
|
-
include Familia::Horreum::RelatedFieldsManagement
|
47
|
+
include Familia::Horreum::RelatedFieldsManagement # Provides DataType field methods
|
39
48
|
|
40
49
|
# Sets or retrieves the unique identifier field for the class.
|
41
50
|
#
|
@@ -86,12 +95,12 @@ module Familia
|
|
86
95
|
# - Others, depending on features available
|
87
96
|
#
|
88
97
|
def field(name, as: name, fast_method: :"#{name}!", on_conflict: :raise, category: nil)
|
89
|
-
# Use field type system
|
90
|
-
require_relative '
|
98
|
+
# Use field type system for consistency
|
99
|
+
require_relative '../../field_type'
|
91
100
|
|
92
101
|
# Create appropriate field type based on category
|
93
102
|
field_type = if category == :transient
|
94
|
-
require_relative '
|
103
|
+
require_relative '../../features/transient_fields/transient_field_type'
|
95
104
|
TransientFieldType.new(name, as: as, fast_method: false, on_conflict: on_conflict)
|
96
105
|
else
|
97
106
|
# For regular fields and other categories, create custom field type with category override
|
@@ -232,7 +241,7 @@ module Familia
|
|
232
241
|
# @param options [Hash] Field options
|
233
242
|
#
|
234
243
|
def transient_field(name, **)
|
235
|
-
require_relative '
|
244
|
+
require_relative '../../features/transient_fields/transient_field_type'
|
236
245
|
field_type = TransientFieldType.new(name, **, fast_method: false)
|
237
246
|
register_field_type(field_type)
|
238
247
|
end
|
@@ -261,7 +270,7 @@ module Familia
|
|
261
270
|
WARNING
|
262
271
|
when :raise
|
263
272
|
raise ArgumentError, "Method >>> #{method_name} <<< already defined for #{self}"
|
264
|
-
when :skip
|
273
|
+
when :skip, :ignore
|
265
274
|
# Do nothing, skip silently
|
266
275
|
end
|
267
276
|
end
|
@@ -279,19 +288,26 @@ module Familia
|
|
279
288
|
end
|
280
289
|
end
|
281
290
|
|
282
|
-
#
|
283
|
-
#
|
284
|
-
#
|
285
|
-
#
|
286
|
-
#
|
291
|
+
# Fast attribute accessor method for immediate DB persistence.
|
292
|
+
#
|
293
|
+
# @param field_name [Symbol, String] the name of the horreum model attribute
|
294
|
+
# @param method_name [Symbol, String] the name of the regular accessor method
|
295
|
+
# @param fast_method_name [Symbol, String] the name of the fast method (must end with '!')
|
296
|
+
# @param on_conflict [Symbol] conflict resolution strategy for method name conflicts
|
287
297
|
#
|
288
|
-
# @
|
289
|
-
#
|
290
|
-
# @
|
291
|
-
#
|
292
|
-
# @
|
293
|
-
#
|
294
|
-
#
|
298
|
+
# @return [void]
|
299
|
+
#
|
300
|
+
# @raise [ArgumentError] if fast_method_name doesn't end with '!'
|
301
|
+
#
|
302
|
+
# @note Generated method behavior:
|
303
|
+
# - Without args: Retrieves current value from Redis
|
304
|
+
# - With value: Sets and immediately persists to Redis
|
305
|
+
# - Returns boolean indicating success for writes
|
306
|
+
# - Bypasses object-level caching and expiration updates
|
307
|
+
#
|
308
|
+
# @example
|
309
|
+
# # Creates a method like: username!(value = nil)
|
310
|
+
# define_fast_writer_method(:username, :username, :username!, :raise)
|
295
311
|
#
|
296
312
|
def define_fast_writer_method(field_name, method_name, fast_method_name, on_conflict)
|
297
313
|
raise ArgumentError, 'Must end with !' unless fast_method_name.to_s.end_with?('!')
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/horreum/
|
1
|
+
# lib/familia/horreum/subclass/management.rb
|
2
2
|
|
3
3
|
require_relative 'related_fields_management'
|
4
4
|
|
@@ -15,7 +15,7 @@ module Familia
|
|
15
15
|
# * Provides utility methods for working with Database objects
|
16
16
|
#
|
17
17
|
module ManagementMethods
|
18
|
-
include Familia::Horreum::RelatedFieldsManagement
|
18
|
+
include Familia::Horreum::RelatedFieldsManagement # Provides DataType query methods
|
19
19
|
|
20
20
|
# Creates and persists a new instance of the class.
|
21
21
|
#
|
@@ -55,9 +55,7 @@ module Familia
|
|
55
55
|
#
|
56
56
|
def create(*, **)
|
57
57
|
fobj = new(*, **)
|
58
|
-
|
59
|
-
|
60
|
-
fobj.save
|
58
|
+
fobj.save_if_not_exists
|
61
59
|
fobj
|
62
60
|
end
|
63
61
|
|
@@ -172,8 +170,8 @@ module Familia
|
|
172
170
|
# User.exists?(123) # Returns true if user:123:object exists in Redis
|
173
171
|
#
|
174
172
|
def exists?(identifier, suffix = nil)
|
173
|
+
raise NoIdentifier, "Empty identifier" if identifier.to_s.empty?
|
175
174
|
suffix ||= self.suffix
|
176
|
-
return false if identifier.to_s.empty?
|
177
175
|
|
178
176
|
objkey = dbkey identifier, suffix
|
179
177
|
|
@@ -234,9 +232,11 @@ module Familia
|
|
234
232
|
# distinction b/c passing in an explicitly nil is how DataType objects
|
235
233
|
# at the class level are created without the global default 'object'
|
236
234
|
# suffix. See DataType#dbkey "parent_class?" for more details.
|
235
|
+
#
|
237
236
|
def dbkey(identifier, suffix = self.suffix)
|
238
|
-
|
239
|
-
|
237
|
+
if identifier.to_s.empty?
|
238
|
+
raise NoIdentifier, "#{self} requires non-empty identifier, got: #{identifier.inspect}"
|
239
|
+
end
|
240
240
|
|
241
241
|
identifier &&= identifier.to_s
|
242
242
|
Familia.dbkey(prefix, identifier, suffix)
|
@@ -255,6 +255,7 @@ module Familia
|
|
255
255
|
# Returns the number of dbkeys matching the given filter pattern
|
256
256
|
# @param filter [String] dbkey pattern to match (default: '*')
|
257
257
|
# @return [Integer] Number of matching keys
|
258
|
+
#
|
258
259
|
def matching_keys_count(filter = '*')
|
259
260
|
dbclient.keys(dbkey(filter)).compact.size
|
260
261
|
end
|
data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb}
RENAMED
@@ -15,7 +15,7 @@ module Familia
|
|
15
15
|
#
|
16
16
|
# Usage:
|
17
17
|
# Include this module in classes that need DataType management
|
18
|
-
# Call
|
18
|
+
# Call setup_related_fields_accessors to initialize the feature
|
19
19
|
#
|
20
20
|
module RelatedFieldsManagement
|
21
21
|
# A practical flag to indicate that a Horreum member has relations,
|
@@ -23,17 +23,22 @@ module Familia
|
|
23
23
|
@has_relations = nil
|
24
24
|
|
25
25
|
def self.included(base)
|
26
|
-
base.extend(
|
27
|
-
base.
|
26
|
+
base.extend(RelatedFieldsAccessors)
|
27
|
+
base.setup_related_fields_accessors
|
28
28
|
end
|
29
29
|
|
30
|
-
module
|
30
|
+
module RelatedFieldsAccessors
|
31
31
|
# Sets up all DataType related methods
|
32
|
-
# This method
|
32
|
+
# This method generates the following for each registered DataType:
|
33
33
|
#
|
34
|
-
|
34
|
+
# Instance methods: set(), list(), hashkey(), sorted_set(), etc.
|
35
|
+
# Query methods: set?(), list?(), hashkey?(), sorted_set?(), etc.
|
36
|
+
# Collection methods: sets(), lists(), hashkeys(), sorted_sets(), etc.
|
37
|
+
# Class methods: class_set(), class_list(), etc.
|
38
|
+
#
|
39
|
+
def setup_related_fields_accessors
|
35
40
|
Familia::DataType.registered_types.each_pair do |kind, klass|
|
36
|
-
Familia.
|
41
|
+
Familia.trace :registered_types, kind, klass, caller(1..1) if Familia.debug?
|
37
42
|
|
38
43
|
# Dynamically define instance-level relation methods
|
39
44
|
#
|
@@ -86,11 +91,11 @@ module Familia
|
|
86
91
|
end
|
87
92
|
end
|
88
93
|
end
|
89
|
-
# End of
|
94
|
+
# End of RelatedFieldsAccessors module
|
90
95
|
|
91
96
|
# Creates an instance-level relation
|
92
97
|
def attach_instance_related_field(name, klass, opts)
|
93
|
-
Familia.
|
98
|
+
Familia.trace :attach_instance, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
|
94
99
|
raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
|
95
100
|
|
96
101
|
name = name.to_s.to_sym
|
@@ -115,7 +120,7 @@ module Familia
|
|
115
120
|
|
116
121
|
# Creates a class-level relation
|
117
122
|
def attach_class_related_field(name, klass, opts)
|
118
|
-
Familia.
|
123
|
+
Familia.trace :attach_class_related_field, "#{name} #{klass}", opts, caller(1..1) if Familia.debug?
|
119
124
|
raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
|
120
125
|
|
121
126
|
name = name.to_s.to_sym
|
data/lib/familia/horreum.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
# lib/familia/horreum.rb
|
2
2
|
|
3
|
+
require_relative 'horreum/subclass/definition'
|
4
|
+
require_relative 'horreum/subclass/management'
|
5
|
+
require_relative 'horreum/shared/settings'
|
6
|
+
require_relative 'horreum/core'
|
7
|
+
|
3
8
|
module Familia
|
4
9
|
#
|
5
10
|
# Horreum: A module for managing Redis-based object storage and relationships
|
@@ -23,6 +28,8 @@ module Familia
|
|
23
28
|
#
|
24
29
|
class Horreum
|
25
30
|
include Familia::Base
|
31
|
+
include Familia::Horreum::Core
|
32
|
+
include Familia::Horreum::Settings
|
26
33
|
|
27
34
|
# Singleton Class Context
|
28
35
|
#
|
@@ -62,13 +69,14 @@ module Familia
|
|
62
69
|
# Extends ClassMethods to subclasses and tracks Familia members
|
63
70
|
def inherited(member)
|
64
71
|
Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
|
65
|
-
member.extend(DefinitionMethods)
|
66
|
-
member.extend(ManagementMethods)
|
67
|
-
member.extend(Connection)
|
68
|
-
member.extend(Features)
|
69
72
|
|
70
|
-
#
|
71
|
-
#
|
73
|
+
# Class-level functionality extensions:
|
74
|
+
member.extend(Familia::Horreum::DefinitionMethods) # field(), identifier_field(), dbkey()
|
75
|
+
member.extend(Familia::Horreum::ManagementMethods) # create(), find(), destroy!()
|
76
|
+
member.extend(Familia::Horreum::Connection) # dbclient, connection management
|
77
|
+
member.extend(Familia::Features) # feature() method for optional modules
|
78
|
+
|
79
|
+
# Track all classes that inherit from Horreum
|
72
80
|
Familia.members << member
|
73
81
|
super
|
74
82
|
end
|
@@ -84,7 +92,7 @@ module Familia
|
|
84
92
|
# Session.new({sessid: "abc123", custid: "user456"}) # legacy hash (robust)
|
85
93
|
#
|
86
94
|
def initialize(*args, **kwargs)
|
87
|
-
Familia.
|
95
|
+
Familia.trace :INITIALIZE, dbclient, "Initializing #{self.class}", caller(1..1) if Familia.debug?
|
88
96
|
initialize_relatives
|
89
97
|
|
90
98
|
# No longer auto-create a key field - the identifier method will
|
@@ -123,7 +131,7 @@ module Familia
|
|
123
131
|
elsif args.any?
|
124
132
|
initialize_with_positional_args(*args)
|
125
133
|
else
|
126
|
-
|
134
|
+
Familia.trace :INITIALIZE, dbclient, "#{self.class} initialized with no arguments", caller(1..1) if Familia.debug?
|
127
135
|
# Default values are intentionally NOT set here to:
|
128
136
|
# - Maintain Database memory efficiency (only store non-nil values)
|
129
137
|
# - Avoid conflicts with nil-skipping serialization logic
|
@@ -158,7 +166,7 @@ module Familia
|
|
158
166
|
self.class.related_fields.each_pair do |name, data_type_definition|
|
159
167
|
klass = data_type_definition.klass
|
160
168
|
opts = data_type_definition.opts
|
161
|
-
Familia.
|
169
|
+
Familia.trace :INITIALIZE_RELATIVES, dbclient, "#{name} => #{klass} #{opts.keys}", caller(1..1) if Familia.debug?
|
162
170
|
|
163
171
|
# As a subclass of Familia::Horreum, we add ourselves as the parent
|
164
172
|
# automatically. This is what determines the dbkey for DataType
|
@@ -310,11 +318,3 @@ module Familia
|
|
310
318
|
end
|
311
319
|
end
|
312
320
|
end
|
313
|
-
|
314
|
-
require_relative 'horreum/definition_methods'
|
315
|
-
require_relative 'horreum/management_methods'
|
316
|
-
require_relative 'horreum/database_commands'
|
317
|
-
require_relative 'horreum/connection'
|
318
|
-
require_relative 'horreum/serialization'
|
319
|
-
require_relative 'horreum/settings'
|
320
|
-
require_relative 'horreum/utils'
|
data/lib/familia/version.rb
CHANGED
data/lib/familia.rb
CHANGED
@@ -0,0 +1,240 @@
|
|
1
|
+
# try/core/create_method_try.rb
|
2
|
+
#
|
3
|
+
# Comprehensive test coverage for the create method
|
4
|
+
# Tests the correct exception type and error message handling
|
5
|
+
|
6
|
+
require_relative '../helpers/test_helpers'
|
7
|
+
|
8
|
+
# Test class for create method behavior
|
9
|
+
class CreateTestModel < Familia::Horreum
|
10
|
+
identifier_field :id
|
11
|
+
field :id
|
12
|
+
field :name
|
13
|
+
field :value
|
14
|
+
end
|
15
|
+
|
16
|
+
# Clean up any existing test data
|
17
|
+
cleanup_keys = []
|
18
|
+
begin
|
19
|
+
existing_test_keys = Familia.dbclient.keys('createtestmodel:*')
|
20
|
+
cleanup_keys.concat(existing_test_keys)
|
21
|
+
Familia.dbclient.del(*existing_test_keys) if existing_test_keys.any?
|
22
|
+
rescue => e
|
23
|
+
# Ignore cleanup errors
|
24
|
+
end
|
25
|
+
|
26
|
+
@test_id_counter = 0
|
27
|
+
def next_test_id
|
28
|
+
@test_id_counter += 1
|
29
|
+
"create-test-#{Time.now.to_i}-#{@test_id_counter}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# =============================================
|
33
|
+
# 1. Basic create method functionality
|
34
|
+
# =============================================
|
35
|
+
|
36
|
+
## create method successfully creates new object
|
37
|
+
@test_id = next_test_id
|
38
|
+
@created_obj = CreateTestModel.create(id: @test_id, name: 'Created Object', value: 'test_value')
|
39
|
+
[@created_obj.class, @created_obj.exists?, @created_obj.name]
|
40
|
+
#=> [CreateTestModel, true, 'Created Object']
|
41
|
+
|
42
|
+
## create method returns the created object
|
43
|
+
@created_obj.is_a?(CreateTestModel)
|
44
|
+
#=> true
|
45
|
+
|
46
|
+
## create method persists object fields
|
47
|
+
@created_obj.refresh!
|
48
|
+
[@created_obj.name, @created_obj.value]
|
49
|
+
#=> ['Created Object', 'test_value']
|
50
|
+
|
51
|
+
# =============================================
|
52
|
+
# 2. Duplicate creation error handling
|
53
|
+
# =============================================
|
54
|
+
|
55
|
+
## create method raises RecordExistsError for duplicate
|
56
|
+
begin
|
57
|
+
CreateTestModel.create(id: @test_id, name: 'Duplicate Attempt')
|
58
|
+
false # Should not reach here
|
59
|
+
rescue => e
|
60
|
+
e.class
|
61
|
+
end
|
62
|
+
#=> Familia::RecordExistsError
|
63
|
+
|
64
|
+
## RecordExistsError includes the dbkey in the message
|
65
|
+
begin
|
66
|
+
CreateTestModel.create(id: @test_id, name: 'Another Duplicate')
|
67
|
+
false # Should not reach here
|
68
|
+
rescue Familia::RecordExistsError => e
|
69
|
+
expected_dbkey = "createtestmodel:#{@test_id}:object"
|
70
|
+
e.message.include?(expected_dbkey)
|
71
|
+
end
|
72
|
+
#=> true
|
73
|
+
|
74
|
+
## RecordExistsError message follows consistent format
|
75
|
+
begin
|
76
|
+
CreateTestModel.create(id: @test_id, name: 'Yet Another Duplicate')
|
77
|
+
false # Should not reach here
|
78
|
+
rescue Familia::RecordExistsError => e
|
79
|
+
e.message.start_with?('Key already exists:')
|
80
|
+
end
|
81
|
+
#=> true
|
82
|
+
|
83
|
+
## RecordExistsError exposes key property for programmatic access
|
84
|
+
@final_test_id = next_test_id
|
85
|
+
CreateTestModel.create(id: @final_test_id, name: 'Setup for Key Test')
|
86
|
+
|
87
|
+
begin
|
88
|
+
CreateTestModel.create(id: @final_test_id, name: 'Key Test Duplicate')
|
89
|
+
false # Should not reach here
|
90
|
+
rescue Familia::RecordExistsError => e
|
91
|
+
# Key should be accessible and contain the identifier
|
92
|
+
[e.respond_to?(:key), e.key.include?(@final_test_id)]
|
93
|
+
end
|
94
|
+
#=> [true, true]
|
95
|
+
|
96
|
+
# =============================================
|
97
|
+
# 3. Edge cases and error conditions
|
98
|
+
# =============================================
|
99
|
+
|
100
|
+
## create with empty identifier raises NoIdentifier error
|
101
|
+
CreateTestModel.create(id: '')
|
102
|
+
#=!> Familia::NoIdentifier
|
103
|
+
|
104
|
+
## create with nil identifier raises NoIdentifier error
|
105
|
+
CreateTestModel.create(id: nil)
|
106
|
+
#=!> Familia::NoIdentifier
|
107
|
+
|
108
|
+
## create with only some fields set
|
109
|
+
@partial_id = next_test_id
|
110
|
+
@partial_obj = CreateTestModel.create(id: @partial_id, name: 'Partial Object')
|
111
|
+
[@partial_obj.exists?, @partial_obj.name, @partial_obj.value]
|
112
|
+
#=> [true, 'Partial Object', nil]
|
113
|
+
|
114
|
+
## create with no additional fields (only identifier)
|
115
|
+
@minimal_id = next_test_id
|
116
|
+
@minimal_obj = CreateTestModel.create(id: @minimal_id)
|
117
|
+
[@minimal_obj.exists?, @minimal_obj.id]
|
118
|
+
#=> [true, @minimal_id]
|
119
|
+
|
120
|
+
# =============================================
|
121
|
+
# 4. Concurrency and transaction behavior
|
122
|
+
# =============================================
|
123
|
+
|
124
|
+
## create is atomic - no partial state on failure
|
125
|
+
@concurrent_id = next_test_id
|
126
|
+
@first_obj = CreateTestModel.create(id: @concurrent_id, name: 'First')
|
127
|
+
|
128
|
+
# Verify first object exists
|
129
|
+
first_exists = @first_obj.exists?
|
130
|
+
|
131
|
+
# Attempt to create duplicate should not affect existing object
|
132
|
+
begin
|
133
|
+
CreateTestModel.create(id: @concurrent_id, name: 'Concurrent Attempt')
|
134
|
+
false # Should not reach here
|
135
|
+
rescue Familia::RecordExistsError
|
136
|
+
# Original object should be unchanged
|
137
|
+
@first_obj.refresh!
|
138
|
+
@first_obj.name == 'First'
|
139
|
+
end
|
140
|
+
#=> true
|
141
|
+
|
142
|
+
## create failure doesn't leave partial data
|
143
|
+
before_failed_create = Familia.dbclient.keys("createtestmodel:#{@concurrent_id}:*").length
|
144
|
+
begin
|
145
|
+
CreateTestModel.create(id: @concurrent_id, name: 'Should Fail')
|
146
|
+
rescue Familia::RecordExistsError
|
147
|
+
# Should not create any additional keys
|
148
|
+
after_failed_create = Familia.dbclient.keys("createtestmodel:#{@concurrent_id}:*").length
|
149
|
+
after_failed_create == before_failed_create
|
150
|
+
end
|
151
|
+
#=> true
|
152
|
+
|
153
|
+
# =============================================
|
154
|
+
# 5. Consistency with save_if_not_exists
|
155
|
+
# =============================================
|
156
|
+
|
157
|
+
## Both create and save_if_not_exists raise same error type for duplicates
|
158
|
+
@consistency_id = next_test_id
|
159
|
+
@consistency_obj = CreateTestModel.create(id: @consistency_id, name: 'Consistency Test')
|
160
|
+
|
161
|
+
# Test create raises RecordExistsError
|
162
|
+
create_error_class = begin
|
163
|
+
CreateTestModel.create(id: @consistency_id, name: 'Create Duplicate')
|
164
|
+
nil
|
165
|
+
rescue => e
|
166
|
+
e.class
|
167
|
+
end
|
168
|
+
|
169
|
+
# Test save_if_not_exists raises RecordExistsError
|
170
|
+
sine_error_class = begin
|
171
|
+
duplicate_obj = CreateTestModel.new(id: @consistency_id, name: 'SINE Duplicate')
|
172
|
+
duplicate_obj.save_if_not_exists
|
173
|
+
nil
|
174
|
+
rescue => e
|
175
|
+
e.class
|
176
|
+
end
|
177
|
+
|
178
|
+
[create_error_class, sine_error_class]
|
179
|
+
#=> [Familia::RecordExistsError, Familia::RecordExistsError]
|
180
|
+
|
181
|
+
## Both methods have similar error message patterns
|
182
|
+
@error_comparison_id = next_test_id
|
183
|
+
CreateTestModel.create(id: @error_comparison_id, name: 'Error Comparison')
|
184
|
+
|
185
|
+
create_error_msg = begin
|
186
|
+
CreateTestModel.create(id: @error_comparison_id, name: 'Create Error')
|
187
|
+
nil
|
188
|
+
rescue => e
|
189
|
+
e.message
|
190
|
+
end
|
191
|
+
|
192
|
+
sine_error_msg = begin
|
193
|
+
CreateTestModel.new(id: @error_comparison_id, name: 'SINE Error').save_if_not_exists
|
194
|
+
nil
|
195
|
+
rescue => e
|
196
|
+
e.message
|
197
|
+
end
|
198
|
+
|
199
|
+
# Both should reference the same key concept
|
200
|
+
[create_error_msg.include?('already exists'), sine_error_msg.include?('already exists')]
|
201
|
+
#=> [true, true]
|
202
|
+
|
203
|
+
# =============================================
|
204
|
+
# 6. Integration with different field types
|
205
|
+
# =============================================
|
206
|
+
|
207
|
+
## create works with complex field values
|
208
|
+
@complex_id = next_test_id
|
209
|
+
@complex_obj = CreateTestModel.create(
|
210
|
+
id: @complex_id,
|
211
|
+
name: 'Complex Object',
|
212
|
+
value: { nested: 'data', array: [1, 2, 3] }
|
213
|
+
)
|
214
|
+
[@complex_obj.exists?, @complex_obj.value[:nested]]
|
215
|
+
#=> [true, 'data']
|
216
|
+
|
217
|
+
# =============================================
|
218
|
+
# 7. Class vs instance method consistency
|
219
|
+
# =============================================
|
220
|
+
|
221
|
+
## Class.create and instance.save_if_not_exists have consistent existence checking
|
222
|
+
@consistency_check_id = next_test_id
|
223
|
+
|
224
|
+
# Create via class method
|
225
|
+
@class_created = CreateTestModel.create(id: @consistency_check_id, name: 'Class Created')
|
226
|
+
|
227
|
+
# Both class and instance methods should see the object as existing
|
228
|
+
class_sees_exists = CreateTestModel.exists?(@consistency_check_id)
|
229
|
+
instance_sees_exists = @class_created.exists?
|
230
|
+
|
231
|
+
[class_sees_exists, instance_sees_exists]
|
232
|
+
#=> [true, true]
|
233
|
+
|
234
|
+
# =============================================
|
235
|
+
# Cleanup
|
236
|
+
# =============================================
|
237
|
+
|
238
|
+
# Clean up all test data
|
239
|
+
test_keys = Familia.dbclient.keys('createtestmodel:*')
|
240
|
+
Familia.dbclient.del(*test_keys) if test_keys.any?
|