familia 2.0.0.pre17 → 2.0.0.pre18
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/CHANGELOG.rst +60 -0
- data/CLAUDE.md +9 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/qodo-merge-compliance.md +96 -0
- data/lib/familia/base.rb +0 -2
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/{commands.rb → database_commands.rb} +2 -2
- data/lib/familia/data_type/serialization.rb +5 -5
- data/lib/familia/data_type.rb +2 -2
- data/lib/familia/encryption/encrypted_data.rb +12 -2
- data/lib/familia/encryption/manager.rb +11 -4
- data/lib/familia/features/autoloader.rb +3 -1
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +11 -3
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +41 -27
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/horreum/database_commands.rb +1 -1
- data/lib/familia/horreum/definition.rb +6 -37
- data/lib/familia/horreum/management.rb +17 -12
- data/lib/familia/horreum/persistence.rb +1 -1
- data/lib/familia/horreum/serialization.rb +91 -73
- data/lib/familia/horreum.rb +10 -6
- data/lib/familia/identifier_extractor.rb +60 -0
- data/lib/familia/logging.rb +271 -112
- data/lib/familia/refinements.rb +0 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -2
- data/lib/middleware/{database_middleware.rb → database_logger.rb} +47 -14
- data/pr_agent.toml +31 -0
- data/pr_compliance_checklist.yaml +45 -0
- data/try/edge_cases/empty_identifiers_try.rb +1 -1
- data/try/edge_cases/hash_symbolization_try.rb +31 -31
- data/try/edge_cases/json_serialization_try.rb +2 -2
- data/try/edge_cases/legacy_data_detection/deserialization_edge_cases_try.rb +170 -0
- data/try/edge_cases/race_conditions_try.rb +1 -1
- data/try/edge_cases/reserved_keywords_try.rb +1 -1
- data/try/edge_cases/string_coercion_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/features/encrypted_fields/aad_protection_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +1 -1
- data/try/features/encrypted_fields/context_isolation_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +1 -1
- data/try/features/encrypted_fields/error_conditions_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_derivation_try.rb +1 -1
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/key_rotation_try.rb +1 -1
- data/try/features/encrypted_fields/memory_security_try.rb +1 -1
- data/try/features/encrypted_fields/missing_current_key_version_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +1 -1
- data/try/features/encrypted_fields/thread_safety_try.rb +1 -1
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +1 -1
- data/try/{encryption → features/encryption}/config_persistence_try.rb +1 -1
- data/try/{encryption/encryption_core_try.rb → features/encryption/core_try.rb} +2 -2
- data/try/{encryption → features/encryption}/instance_variable_scope_try.rb +1 -1
- data/try/{encryption → features/encryption}/module_loading_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/aes_gcm_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/{encryption → features/encryption}/roundtrip_validation_try.rb +1 -1
- data/try/{encryption → features/encryption}/secure_memory_handling_try.rb +2 -2
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +1 -1
- data/try/features/feature_improvements_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_integration_try.rb +1 -1
- data/try/features/object_identifier/object_identifier_try.rb +1 -1
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/real_feature_integration_try.rb +17 -14
- data/try/features/relationships/indexing_commands_verification_try.rb +8 -3
- data/try/features/relationships/indexing_try.rb +6 -1
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +4 -4
- data/try/features/relationships/participation_performance_improvements_try.rb +1 -1
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +1 -1
- data/try/features/relationships/relationships_edge_cases_try.rb +3 -3
- data/try/features/relationships/relationships_performance_minimal_try.rb +1 -1
- data/try/features/relationships/relationships_performance_simple_try.rb +1 -1
- data/try/features/relationships/relationships_performance_try.rb +1 -1
- data/try/features/relationships/relationships_performance_working_try.rb +1 -1
- data/try/features/relationships/relationships_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_advanced_try.rb +1 -1
- data/try/features/safe_dump/safe_dump_try.rb +1 -1
- data/try/features/transient_fields/redacted_string_try.rb +1 -1
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/single_use_redacted_string_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields/transient_fields_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/fiber_context_preservation_try.rb +1 -1
- data/try/{connection → integration/connection}/handler_constraints_try.rb +1 -1
- data/try/{core → integration/connection}/isolated_dbclient_try.rb +1 -1
- data/try/integration/connection/middleware_reconnect_try.rb +87 -0
- data/try/{connection → integration/connection}/operation_mode_guards_try.rb +1 -1
- data/try/{connection → integration/connection}/pipeline_fallback_integration_try.rb +1 -1
- data/try/{core → integration/connection}/pools_try.rb +1 -1
- data/try/{connection → integration/connection}/responsibility_chain_tracking_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_fallback_integration_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_permissive_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_strict_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_mode_warn_try.rb +1 -1
- data/try/{connection → integration/connection}/transaction_modes_try.rb +1 -1
- data/try/{core → integration}/conventional_inheritance_try.rb +1 -1
- data/try/{core → integration}/create_method_try.rb +1 -1
- data/try/integration/cross_component_try.rb +1 -1
- data/try/{core → integration}/database_consistency_try.rb +11 -8
- data/try/{core → integration}/familia_extended_try.rb +1 -1
- data/try/{core → integration}/familia_members_methods_try.rb +1 -1
- data/try/{models → integration/models}/customer_safe_dump_try.rb +1 -1
- data/try/{models → integration/models}/customer_try.rb +1 -1
- data/try/{models → integration/models}/datatype_base_try.rb +1 -1
- data/try/{models → integration/models}/familia_object_try.rb +1 -1
- data/try/{core → integration}/persistence_operations_try.rb +1 -1
- data/try/integration/relationships_persistence_round_trip_try.rb +441 -0
- data/try/{configuration → integration}/scenarios_try.rb +1 -1
- data/try/{core → integration}/secure_identifier_try.rb +1 -1
- data/try/{core → integration}/verifiable_identifier_try.rb +1 -1
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/support/benchmarks/deserialization_benchmark.rb +180 -0
- data/try/support/benchmarks/deserialization_correctness_test.rb +237 -0
- data/try/{helpers → support/helpers}/test_helpers.rb +12 -3
- data/try/{core → unit/core}/autoloader_try.rb +1 -1
- data/try/{core → unit/core}/base_enhancements_try.rb +1 -9
- data/try/{core → unit/core}/connection_try.rb +1 -1
- data/try/{core → unit/core}/errors_try.rb +1 -1
- data/try/{core → unit/core}/extensions_try.rb +1 -1
- data/try/unit/core/familia_logger_try.rb +110 -0
- data/try/{core → unit/core}/familia_try.rb +1 -1
- data/try/{core → unit/core}/middleware_try.rb +41 -1
- data/try/{core → unit/core}/settings_try.rb +1 -1
- data/try/{core → unit/core}/time_utils_try.rb +1 -1
- data/try/{core → unit/core}/tools_try.rb +1 -1
- data/try/{core → unit/core}/utils_try.rb +17 -14
- data/try/{data_types → unit/data_types}/boolean_try.rb +1 -1
- data/try/{data_types → unit/data_types}/counter_try.rb +1 -1
- data/try/{data_types → unit/data_types}/datatype_base_try.rb +1 -1
- data/try/{data_types → unit/data_types}/hash_try.rb +1 -1
- data/try/{data_types → unit/data_types}/list_try.rb +1 -1
- data/try/{data_types → unit/data_types}/lock_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_try.rb +1 -1
- data/try/{data_types → unit/data_types}/sorted_set_zadd_options_try.rb +1 -1
- data/try/{data_types → unit/data_types}/string_try.rb +1 -1
- data/try/{data_types → unit/data_types}/unsortedset_try.rb +1 -1
- data/try/{horreum → unit/horreum}/auto_indexing_on_save_try.rb +1 -1
- data/try/{horreum → unit/horreum}/base_try.rb +3 -3
- data/try/{horreum → unit/horreum}/class_methods_try.rb +1 -1
- data/try/{horreum → unit/horreum}/commands_try.rb +1 -1
- data/try/{horreum → unit/horreum}/defensive_initialization_try.rb +1 -1
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +1 -1
- data/try/{horreum → unit/horreum}/enhanced_conflict_handling_try.rb +1 -1
- data/try/{horreum → unit/horreum}/field_categories_try.rb +27 -18
- data/try/{horreum → unit/horreum}/field_definition_try.rb +1 -1
- data/try/{horreum → unit/horreum}/initialization_try.rb +2 -2
- data/try/unit/horreum/json_type_preservation_try.rb +248 -0
- data/try/{horreum → unit/horreum}/relations_try.rb +1 -1
- data/try/{horreum → unit/horreum}/serialization_persistent_fields_try.rb +24 -18
- data/try/{horreum → unit/horreum}/serialization_try.rb +4 -4
- data/try/{horreum → unit/horreum}/settings_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_array_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/dear_json_hash_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_numeric_methods_try.rb +1 -1
- data/try/{refinements → unit/refinements}/time_literals_string_methods_try.rb +1 -1
- metadata +134 -125
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/try/{debugging → support/debugging}/README.md +0 -0
- /data/try/{debugging → support/debugging}/cache_behavior_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_aad_process.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_internal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_concealed_reveal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_aad.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_context_simple.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_database_load.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_check.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_encrypted_json_step_by_step.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_exists_lifecycle.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_field_decrypt.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_fresh_cross_context.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_load_path.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_definition.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_method_resolution.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_minimal.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_provider.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_secure_behavior.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_string_class.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test.rb +0 -0
- /data/try/{debugging → support/debugging}/debug_test_design.rb +0 -0
- /data/try/{debugging → support/debugging}/encryption_method_tracer.rb +0 -0
- /data/try/{debugging → support/debugging}/provider_diagnostics.rb +0 -0
- /data/try/{helpers → support/helpers}/test_cleanup.rb +0 -0
- /data/try/{memory → support/memory}/memory_basic_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_detailed_test.rb +0 -0
- /data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +0 -0
- /data/try/{memory → support/memory}/memory_search_for_string.rb +0 -0
- /data/try/{memory → support/memory}/test_actual_redactedstring_protection.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v1_context_proxy.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v2_connection_switching.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v3_connection_pool.rb +0 -0
- /data/try/{prototypes → support/prototypes}/atomic_saves_v4.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v2_connection_switching_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/README.md +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/configurable_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_metrics.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_stress_test.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/connection_pool_threading_models.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/lib/visualize_stress_results.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/pool_siege.rb +0 -0
- /data/try/{prototypes → support/prototypes}/pooling/run_stress_tests.rb +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/core/utils_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -62,26 +62,29 @@ custom_stamp = Familia.qstamp(3600, time: test_time)
|
|
62
62
|
Time.at(custom_stamp).utc.hour
|
63
63
|
#=> 14
|
64
64
|
|
65
|
-
##
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
#=> ["test", "123", "symbol"]
|
65
|
+
## identifier_extractor extracts class names
|
66
|
+
test_class = Class.new(Familia::Horreum)
|
67
|
+
test_class.define_singleton_method(:name) { 'TestClass' }
|
68
|
+
Familia.identifier_extractor(test_class)
|
69
|
+
#=> "TestClass"
|
71
70
|
|
72
|
-
##
|
71
|
+
## identifier_extractor extracts identifiers from Familia objects
|
72
|
+
customer_class = Class.new(Familia::Horreum) do
|
73
|
+
identifier_field :custid
|
74
|
+
field :custid
|
75
|
+
end
|
76
|
+
customer = customer_class.new(custid: 'customer_123')
|
77
|
+
Familia.identifier_extractor(customer)
|
78
|
+
#=> "customer_123"
|
79
|
+
|
80
|
+
## identifier_extractor raises error for non-Familia objects
|
73
81
|
begin
|
74
|
-
Familia.
|
82
|
+
Familia.identifier_extractor({ key: 'value' })
|
75
83
|
rescue Familia::NotDistinguishableError => e
|
76
84
|
e.class
|
77
85
|
end
|
78
86
|
#=> Familia::NotDistinguishableError
|
79
87
|
|
80
|
-
## distinguisher allows high-risk types with non-strict mode
|
81
|
-
result = Familia.distinguisher(false, strict_values: false)
|
82
|
-
result
|
83
|
-
#=> "false"
|
84
|
-
|
85
88
|
# Cleanup - restore defaults, leave nothing but footprints
|
86
89
|
Familia.delim(':')
|
87
90
|
Familia.suffix(:object)
|
@@ -5,7 +5,7 @@
|
|
5
5
|
# Tests automatic index population when Familia::Horreum objects are saved
|
6
6
|
#
|
7
7
|
|
8
|
-
require_relative '
|
8
|
+
require_relative '../../support/helpers/test_helpers'
|
9
9
|
|
10
10
|
# Test classes for auto-indexing functionality
|
11
11
|
class ::AutoIndexUser < Familia::Horreum
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/horreum/base_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -203,11 +203,11 @@ end
|
|
203
203
|
@aliased.display_size! 100
|
204
204
|
#=> true
|
205
205
|
|
206
|
-
## Aliased field refresh works correctly
|
206
|
+
## Aliased field refresh works correctly (type preserved)
|
207
207
|
@aliased.width = 50 # unsaved change
|
208
208
|
@aliased.refresh!
|
209
209
|
@aliased.width
|
210
|
-
#=>
|
210
|
+
#=> 100
|
211
211
|
|
212
212
|
## Fast method with custom name
|
213
213
|
class CustomFastMethodTest < Familia::Horreum
|
@@ -9,7 +9,7 @@
|
|
9
9
|
# This addresses the bug where destroy! only deleted the main object key
|
10
10
|
# but left related field keys in the database.
|
11
11
|
|
12
|
-
require_relative '
|
12
|
+
require_relative '../../support/helpers/test_helpers'
|
13
13
|
|
14
14
|
MANY_FIELD_MULTIPLIER = 10
|
15
15
|
|
@@ -1,44 +1,53 @@
|
|
1
1
|
# try/horreum/field_categories_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
7
|
-
# Define test class with various field
|
7
|
+
# Define test class with various field types
|
8
8
|
class FieldCategoryTest < Familia::Horreum
|
9
|
+
feature :encrypted_fields
|
10
|
+
feature :transient_fields
|
11
|
+
|
9
12
|
identifier_field :id
|
10
13
|
field :id
|
11
|
-
field :name #
|
12
|
-
|
13
|
-
|
14
|
-
field :description
|
15
|
-
field :settings
|
14
|
+
field :name # regular field
|
15
|
+
encrypted_field :email # encrypted field
|
16
|
+
transient_field :tryouts_cache_data # transient field
|
17
|
+
field :description # regular persistent field
|
18
|
+
field :settings # regular field
|
16
19
|
end
|
17
20
|
|
18
21
|
# Test class with multiple transient fields
|
19
22
|
class MultiTransientTest < Familia::Horreum
|
23
|
+
feature :transient_fields
|
24
|
+
|
20
25
|
identifier_field :id
|
21
26
|
field :id
|
22
27
|
field :permanent_data
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
transient_field :temp1
|
29
|
+
transient_field :temp2
|
30
|
+
transient_field :temp3
|
26
31
|
end
|
27
32
|
|
28
|
-
# Field
|
33
|
+
# Field types work with field aliasing
|
29
34
|
class AliasedCategoryTest < Familia::Horreum
|
35
|
+
feature :transient_fields
|
36
|
+
|
30
37
|
identifier_field :id
|
31
38
|
field :id
|
32
|
-
|
33
|
-
field :internal_perm, as: :perm
|
39
|
+
transient_field :internal_temp, as: :temp
|
40
|
+
field :internal_perm, as: :perm
|
34
41
|
end
|
35
42
|
|
36
43
|
# Test edge case with all transient fields
|
37
44
|
class AllTransientTest < Familia::Horreum
|
45
|
+
feature :transient_fields
|
46
|
+
|
38
47
|
identifier_field :id
|
39
48
|
field :id
|
40
|
-
|
41
|
-
|
49
|
+
transient_field :temp1
|
50
|
+
transient_field :temp2
|
42
51
|
end
|
43
52
|
|
44
53
|
## Field types are stored correctly
|
@@ -58,11 +67,11 @@ FieldCategoryTest.field_types[:email].category
|
|
58
67
|
FieldCategoryTest.field_types[:tryouts_cache_data].category
|
59
68
|
#=> :transient
|
60
69
|
|
61
|
-
##
|
70
|
+
## Regular fields have :field category
|
62
71
|
FieldCategoryTest.field_types[:description].category
|
63
|
-
#=> :
|
72
|
+
#=> :field
|
64
73
|
|
65
|
-
##
|
74
|
+
## Regular fields default to :field category
|
66
75
|
FieldCategoryTest.field_types[:settings].category
|
67
76
|
#=> :field
|
68
77
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/horreum/initialization_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -97,7 +97,7 @@ Familia.debug = false
|
|
97
97
|
@complex.save
|
98
98
|
@complex.refresh!
|
99
99
|
[@complex.custid, @complex.name, @complex.role, @complex.verified]
|
100
|
-
#=> ["complex@test.com", "Complex User", "admin",
|
100
|
+
#=> ["complex@test.com", "Complex User", "admin", true]
|
101
101
|
|
102
102
|
## Clean up saved test objects
|
103
103
|
[@customer6, @complex].map(&:delete!)
|
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
|
+
|
5
|
+
Familia.debug = false
|
6
|
+
|
7
|
+
# Setup: Define test model with various field types
|
8
|
+
class JsonTestModel < Familia::Horreum
|
9
|
+
identifier_field :model_id
|
10
|
+
field :model_id
|
11
|
+
field :name # String
|
12
|
+
field :age # Integer
|
13
|
+
field :active # Boolean
|
14
|
+
field :score # Float
|
15
|
+
field :metadata # Hash
|
16
|
+
field :tags # Array
|
17
|
+
field :nested_data # Hash with nested structures
|
18
|
+
field :badge_number # String that looks numeric
|
19
|
+
field :status # String that looks boolean
|
20
|
+
field :placeholder # String that looks like null
|
21
|
+
end
|
22
|
+
|
23
|
+
@test_id = "json_test_#{Familia.now.to_i}"
|
24
|
+
|
25
|
+
## String fields preserve String type
|
26
|
+
@model = JsonTestModel.new(model_id: @test_id, name: "Test User")
|
27
|
+
@model.save
|
28
|
+
@loaded = JsonTestModel.find(@test_id)
|
29
|
+
@loaded.name
|
30
|
+
#=> "Test User"
|
31
|
+
|
32
|
+
## String field returns String class
|
33
|
+
@loaded.name.class
|
34
|
+
#=> String
|
35
|
+
|
36
|
+
## Integer fields preserve Integer type (not String)
|
37
|
+
@model = JsonTestModel.new(model_id: @test_id, age: 35)
|
38
|
+
@model.save
|
39
|
+
@loaded = JsonTestModel.find(@test_id)
|
40
|
+
@loaded.age
|
41
|
+
#=> 35
|
42
|
+
|
43
|
+
## Integer field returns Integer class (not String)
|
44
|
+
@loaded.age.class
|
45
|
+
#=> Integer
|
46
|
+
|
47
|
+
## Boolean true preserves TrueClass type (not String "true")
|
48
|
+
@model = JsonTestModel.new(model_id: @test_id, active: true)
|
49
|
+
@model.save
|
50
|
+
@loaded = JsonTestModel.find(@test_id)
|
51
|
+
@loaded.active
|
52
|
+
#=> true
|
53
|
+
|
54
|
+
## Boolean true returns TrueClass
|
55
|
+
@loaded.active.class
|
56
|
+
#=> TrueClass
|
57
|
+
|
58
|
+
## Boolean false preserves FalseClass type (not String "false")
|
59
|
+
@model = JsonTestModel.new(model_id: @test_id, active: false)
|
60
|
+
@model.save
|
61
|
+
@loaded = JsonTestModel.find(@test_id)
|
62
|
+
@loaded.active
|
63
|
+
#=> false
|
64
|
+
|
65
|
+
## Boolean false returns FalseClass
|
66
|
+
@loaded.active.class
|
67
|
+
#=> FalseClass
|
68
|
+
|
69
|
+
## Float fields preserve Float type
|
70
|
+
@model = JsonTestModel.new(model_id: @test_id, score: 98.6)
|
71
|
+
@model.save
|
72
|
+
@loaded = JsonTestModel.find(@test_id)
|
73
|
+
@loaded.score
|
74
|
+
#=> 98.6
|
75
|
+
|
76
|
+
## Float field returns Float class
|
77
|
+
@loaded.score.class
|
78
|
+
#=> Float
|
79
|
+
|
80
|
+
## Hash fields preserve Hash structure
|
81
|
+
@model = JsonTestModel.new(model_id: @test_id, metadata: { "key" => "value", "count" => 42 })
|
82
|
+
@model.save
|
83
|
+
@loaded = JsonTestModel.find(@test_id)
|
84
|
+
@loaded.metadata
|
85
|
+
#=> {"key"=>"value", "count"=>42}
|
86
|
+
|
87
|
+
## Hash field returns Hash class
|
88
|
+
@loaded.metadata.class
|
89
|
+
#=> Hash
|
90
|
+
|
91
|
+
## Array fields preserve Array structure and element types
|
92
|
+
@model = JsonTestModel.new(model_id: @test_id, tags: ["ruby", "redis", 123, true])
|
93
|
+
@model.save
|
94
|
+
@loaded = JsonTestModel.find(@test_id)
|
95
|
+
@loaded.tags
|
96
|
+
#=> ["ruby", "redis", 123, true]
|
97
|
+
|
98
|
+
## Array field returns Array class
|
99
|
+
@loaded.tags.class
|
100
|
+
#=> Array
|
101
|
+
|
102
|
+
## Array preserves Integer element type
|
103
|
+
@loaded.tags[2].class
|
104
|
+
#=> Integer
|
105
|
+
|
106
|
+
## Array preserves Boolean element type
|
107
|
+
@loaded.tags[3].class
|
108
|
+
#=> TrueClass
|
109
|
+
|
110
|
+
## Nested Hash structures preserve types at all levels
|
111
|
+
@nested = {
|
112
|
+
"user" => {
|
113
|
+
"name" => "John",
|
114
|
+
"age" => 30,
|
115
|
+
"active" => true,
|
116
|
+
"roles" => ["admin", "user"]
|
117
|
+
},
|
118
|
+
"count" => 5
|
119
|
+
}
|
120
|
+
@model = JsonTestModel.new(model_id: @test_id, nested_data: @nested)
|
121
|
+
@model.save
|
122
|
+
@loaded = JsonTestModel.find(@test_id)
|
123
|
+
@loaded.nested_data["user"]["age"].class
|
124
|
+
#=> Integer
|
125
|
+
|
126
|
+
## Nested Hash Boolean preserved
|
127
|
+
@loaded.nested_data["user"]["active"].class
|
128
|
+
#=> TrueClass
|
129
|
+
|
130
|
+
## Nested Hash top-level Integer preserved
|
131
|
+
@loaded.nested_data["count"].class
|
132
|
+
#=> Integer
|
133
|
+
## Round-trip consistency: save → load → save → load maintains types
|
134
|
+
@model = JsonTestModel.new(
|
135
|
+
model_id: @test_id,
|
136
|
+
name: "Round Trip",
|
137
|
+
age: 42,
|
138
|
+
active: true,
|
139
|
+
score: 99.9,
|
140
|
+
tags: [1, 2, 3]
|
141
|
+
)
|
142
|
+
@model.save
|
143
|
+
@first_load = JsonTestModel.find(@test_id)
|
144
|
+
@first_load.save # Re-save without modification
|
145
|
+
@second_load = JsonTestModel.find(@test_id)
|
146
|
+
[@second_load.age.class, @second_load.active.class, @second_load.tags[0].class]
|
147
|
+
#=> [Integer, TrueClass, Integer]
|
148
|
+
|
149
|
+
## batch_update preserves types
|
150
|
+
@model = JsonTestModel.new(model_id: @test_id)
|
151
|
+
@model.batch_update(name: "Batch", age: 50, active: false, tags: ["one", 2])
|
152
|
+
@loaded = JsonTestModel.find(@test_id)
|
153
|
+
[@loaded.name, @loaded.age, @loaded.active, @loaded.tags[1]]
|
154
|
+
#=> ["Batch", 50, false, 2]
|
155
|
+
|
156
|
+
## batch_update types verification
|
157
|
+
[@loaded.age.class, @loaded.active.class, @loaded.tags[1].class]
|
158
|
+
#=> [Integer, FalseClass, Integer]
|
159
|
+
|
160
|
+
## refresh! maintains type preservation
|
161
|
+
@model = JsonTestModel.new(model_id: @test_id, age: 25, active: true)
|
162
|
+
@model.save
|
163
|
+
# Modify directly in Redis to simulate external change
|
164
|
+
@model.dbclient.hset(@model.dbkey, "age", Familia::JsonSerializer.dump(30))
|
165
|
+
@model.refresh!
|
166
|
+
[@model.age, @model.age.class]
|
167
|
+
#=> [30, Integer]
|
168
|
+
|
169
|
+
## to_h returns string keys (external API compatibility)
|
170
|
+
@model = JsonTestModel.new(model_id: @test_id, name: "API Test", age: 40)
|
171
|
+
@hash = @model.to_h
|
172
|
+
@hash.keys.first.class
|
173
|
+
#=> String
|
174
|
+
|
175
|
+
## to_h has string key for name field
|
176
|
+
@hash.key?("name")
|
177
|
+
#=> true
|
178
|
+
|
179
|
+
## Nil values are handled correctly
|
180
|
+
@model = JsonTestModel.new(model_id: @test_id, name: "Nil Test", age: nil, active: nil)
|
181
|
+
@model.save
|
182
|
+
@loaded = JsonTestModel.find(@test_id)
|
183
|
+
[@loaded.age, @loaded.active]
|
184
|
+
#=> [nil, nil]
|
185
|
+
|
186
|
+
## Empty string preserved correctly
|
187
|
+
@model = JsonTestModel.new(model_id: @test_id, name: "")
|
188
|
+
@model.save
|
189
|
+
@loaded = JsonTestModel.find(@test_id)
|
190
|
+
@loaded.name
|
191
|
+
#=> ""
|
192
|
+
|
193
|
+
## Empty string has String class
|
194
|
+
@loaded.name.class
|
195
|
+
#=> String
|
196
|
+
|
197
|
+
## String fields with numeric values preserve String type (badge "007")
|
198
|
+
@model = JsonTestModel.new(model_id: @test_id, badge_number: "007")
|
199
|
+
@model.save
|
200
|
+
@loaded = JsonTestModel.find(@test_id)
|
201
|
+
@loaded.badge_number
|
202
|
+
#=> "007"
|
203
|
+
|
204
|
+
## Badge number preserves String class (not Integer)
|
205
|
+
@loaded.badge_number.class
|
206
|
+
#=> String
|
207
|
+
|
208
|
+
## String "true" stays String (not Boolean)
|
209
|
+
@model = JsonTestModel.new(model_id: @test_id, status: "true")
|
210
|
+
@model.save
|
211
|
+
@loaded = JsonTestModel.find(@test_id)
|
212
|
+
[@loaded.status, @loaded.status.class]
|
213
|
+
#=> ["true", String]
|
214
|
+
|
215
|
+
## String "false" stays String (not Boolean)
|
216
|
+
@model = JsonTestModel.new(model_id: @test_id, status: "false")
|
217
|
+
@model.save
|
218
|
+
@loaded = JsonTestModel.find(@test_id)
|
219
|
+
[@loaded.status, @loaded.status.class]
|
220
|
+
#=> ["false", String]
|
221
|
+
|
222
|
+
## String "null" stays String (not nil)
|
223
|
+
@model = JsonTestModel.new(model_id: @test_id, placeholder: "null")
|
224
|
+
@model.save
|
225
|
+
@loaded = JsonTestModel.find(@test_id)
|
226
|
+
[@loaded.placeholder, @loaded.placeholder.class]
|
227
|
+
#=> ["null", String]
|
228
|
+
|
229
|
+
## Numeric string "123" preserves String type
|
230
|
+
@model = JsonTestModel.new(model_id: @test_id, badge_number: "123")
|
231
|
+
@model.save
|
232
|
+
@loaded = JsonTestModel.find(@test_id)
|
233
|
+
[@loaded.badge_number, @loaded.badge_number.class]
|
234
|
+
#=> ["123", String]
|
235
|
+
|
236
|
+
## Zero values preserved with correct types
|
237
|
+
@model = JsonTestModel.new(model_id: @test_id, age: 0, score: 0.0)
|
238
|
+
@model.save
|
239
|
+
@loaded = JsonTestModel.find(@test_id)
|
240
|
+
[@loaded.age, @loaded.score]
|
241
|
+
#=> [0, 0.0]
|
242
|
+
|
243
|
+
## Zero Integer and Float have correct classes
|
244
|
+
[@loaded.age.class, @loaded.score.class]
|
245
|
+
#=> [Integer, Float]
|
246
|
+
|
247
|
+
# Teardown: Clean up test data
|
248
|
+
JsonTestModel.destroy!(@test_id) if JsonTestModel.exists?(@test_id)
|