familia 2.0.0.pre16 → 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/.github/workflows/ci.yml +2 -2
- data/.github/workflows/{code-smellage.yml → code-smells.yml} +3 -63
- data/.gitignore +2 -0
- data/.rubocop.yml +6 -0
- data/CHANGELOG.rst +82 -0
- data/CLAUDE.md +47 -2
- data/Gemfile.lock +1 -1
- data/README.md +13 -0
- data/bin/irb +1 -1
- data/docs/archive/FAMILIA_TECHNICAL.md +1 -1
- data/docs/guides/core-field-system.md +48 -26
- data/docs/migrating/v2.0.0-pre18.md +58 -0
- data/docs/overview.md +2 -2
- data/docs/qodo-merge-compliance.md +96 -0
- data/docs/reference/api-technical.md +1 -1
- data/examples/encrypted_fields.rb +1 -1
- data/examples/safe_dump.rb +1 -1
- data/lib/familia/base.rb +6 -6
- data/lib/familia/connection/middleware.rb +58 -4
- data/lib/familia/connection.rb +1 -1
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/connection.rb +83 -0
- 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/settings.rb +96 -0
- data/lib/familia/data_type/types/hashkey.rb +2 -1
- data/lib/familia/data_type/types/sorted_set.rb +113 -10
- data/lib/familia/data_type/types/stringkey.rb +0 -4
- data/lib/familia/data_type.rb +8 -195
- 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/encrypted_fields.rb +5 -2
- data/lib/familia/features/external_identifier.rb +49 -8
- data/lib/familia/features/object_identifier.rb +84 -12
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +9 -9
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +45 -26
- data/lib/familia/features/relationships/indexing.rb +7 -1
- data/lib/familia/features/relationships/participation/participant_methods.rb +6 -2
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features/transient_fields.rb +7 -2
- data/lib/familia/features.rb +6 -1
- data/lib/familia/field_type.rb +0 -18
- data/lib/familia/horreum/{core/connection.rb → connection.rb} +21 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +1 -1
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +102 -56
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +18 -15
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +73 -170
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +22 -2
- data/lib/familia/horreum/serialization.rb +190 -0
- data/lib/familia/horreum.rb +39 -14
- 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/field_groups_try.rb +244 -0
- 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 +16 -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 +3 -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 +3 -3
- 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 +12 -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 +6 -6
- 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 +2 -2
- 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 +15 -7
- data/try/{memory → support/memory}/memory_docker_ruby_dump.sh +1 -1
- 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 +5 -5
- data/try/{core → unit/core}/errors_try.rb +4 -4
- 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 +2 -2
- 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 +3 -3
- 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/unit/data_types/sorted_set_zadd_options_try.rb +625 -0
- 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/unit/horreum/auto_indexing_on_save_try.rb +212 -0
- 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 +3 -1
- data/try/unit/horreum/defensive_initialization_try.rb +86 -0
- data/try/{horreum → unit/horreum}/destroy_related_fields_cleanup_try.rb +3 -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 +3 -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
- data/try/valkey.conf +26 -0
- metadata +149 -132
- data/lib/familia/distinguisher.rb +0 -85
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/logger_trace.rb +0 -60
- data/try/refinements/logger_trace_methods_try.rb +0 -44
- /data/lib/familia/horreum/{shared/settings.rb → settings.rb} +0 -0
- /data/lib/familia/horreum/{core/utils.rb → utils.rb} +0 -0
- /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_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
@@ -0,0 +1,237 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Correctness Test: Field deserialization strategies
|
4
|
+
#
|
5
|
+
# Verifies that different deserialization approaches produce identical
|
6
|
+
# results for all field types (strings, numbers, JSON, nested structures).
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
# $ try/support/benchmarks/deserialization_correctness_test.rb
|
10
|
+
#
|
11
|
+
|
12
|
+
require_relative '../../../lib/familia'
|
13
|
+
require 'json'
|
14
|
+
|
15
|
+
# Setup Redis connection
|
16
|
+
Familia.uri = ENV['REDIS_URI'] || 'redis://localhost:2525/3'
|
17
|
+
|
18
|
+
# Sample model with various field types
|
19
|
+
class CorrectnessTestUser < Familia::Horreum
|
20
|
+
identifier_field :user_id
|
21
|
+
|
22
|
+
field :user_id
|
23
|
+
field :name
|
24
|
+
field :email
|
25
|
+
field :age
|
26
|
+
field :active
|
27
|
+
field :metadata # Will store JSON hash
|
28
|
+
field :tags # Will store JSON array
|
29
|
+
field :created_at # Will store timestamp
|
30
|
+
field :score # Will store float
|
31
|
+
field :simple_string
|
32
|
+
field :nested_data # Will store deeply nested JSON
|
33
|
+
field :empty_string
|
34
|
+
field :nil_value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create sample data with comprehensive test cases
|
38
|
+
sample_data = {
|
39
|
+
'user_id' => 'user_12345',
|
40
|
+
'name' => 'John Doe',
|
41
|
+
'email' => 'john.doe@example.com',
|
42
|
+
'age' => '35',
|
43
|
+
'active' => 'true',
|
44
|
+
'metadata' => '{"role":"admin","department":"engineering","level":5}',
|
45
|
+
'tags' => '["ruby","redis","performance","optimization"]',
|
46
|
+
'created_at' => Time.now.to_i.to_s,
|
47
|
+
'score' => '98.7',
|
48
|
+
'simple_string' => 'Just a plain string value',
|
49
|
+
'nested_data' => '{"user":{"profile":{"settings":{"theme":"dark","notifications":true}}}}',
|
50
|
+
'empty_string' => '',
|
51
|
+
'nil_value' => nil,
|
52
|
+
}
|
53
|
+
|
54
|
+
# Persist sample data to Redis
|
55
|
+
user = CorrectnessTestUser.new(**sample_data)
|
56
|
+
user.save
|
57
|
+
|
58
|
+
# Get the raw hash data directly from Redis
|
59
|
+
raw_hash = CorrectnessTestUser.dbclient.hgetall(user.dbkey)
|
60
|
+
|
61
|
+
puts 'Correctness Test: Deserialization Strategies'
|
62
|
+
puts '=' * 70
|
63
|
+
puts "\nTesting with #{raw_hash.keys.size} fields"
|
64
|
+
puts "\n"
|
65
|
+
|
66
|
+
# Strategy 1: Current field-by-field (reference implementation)
|
67
|
+
def strategy_current(fields, klass)
|
68
|
+
deserialized = fields.transform_values { |value| klass.new.deserialize_value(value) }
|
69
|
+
klass.new(**deserialized)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Strategy 2: Bulk JSON round-trip
|
73
|
+
def strategy_bulk_json(fields, klass)
|
74
|
+
parsed = JSON.parse(JSON.dump(fields))
|
75
|
+
klass.new(**parsed)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Strategy 3: Selective deserialization (only JSON-looking strings)
|
79
|
+
def strategy_selective(fields, klass)
|
80
|
+
deserialized = fields.transform_values do |value|
|
81
|
+
if value.to_s.start_with?('{', '[')
|
82
|
+
begin
|
83
|
+
JSON.parse(value, symbolize_names: true)
|
84
|
+
rescue JSON::ParserError
|
85
|
+
value
|
86
|
+
end
|
87
|
+
else
|
88
|
+
value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
klass.new(**deserialized)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create objects using each strategy
|
95
|
+
current_obj = strategy_current(raw_hash, CorrectnessTestUser)
|
96
|
+
bulk_obj = strategy_bulk_json(raw_hash, CorrectnessTestUser)
|
97
|
+
selective_obj = strategy_selective(raw_hash, CorrectnessTestUser)
|
98
|
+
|
99
|
+
# Test helper to compare field values
|
100
|
+
def compare_values(field, current_val, test_val, strategy_name)
|
101
|
+
current_class = current_val.class
|
102
|
+
test_class = test_val.class
|
103
|
+
|
104
|
+
if current_val == test_val && current_class == test_class
|
105
|
+
{ status: :pass, field: field, strategy: strategy_name }
|
106
|
+
else
|
107
|
+
{
|
108
|
+
status: :fail,
|
109
|
+
field: field,
|
110
|
+
strategy: strategy_name,
|
111
|
+
current: { value: current_val.inspect, class: current_class },
|
112
|
+
test: { value: test_val.inspect, class: test_class },
|
113
|
+
}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Run correctness tests
|
118
|
+
results = []
|
119
|
+
fields_to_test = CorrectnessTestUser.fields
|
120
|
+
|
121
|
+
puts "Testing #{fields_to_test.size} fields across strategies...\n\n"
|
122
|
+
|
123
|
+
# Test Bulk JSON strategy
|
124
|
+
puts 'Strategy: Bulk JSON round-trip'
|
125
|
+
puts '-' * 70
|
126
|
+
fields_to_test.each do |field|
|
127
|
+
current_val = current_obj.send(field)
|
128
|
+
test_val = bulk_obj.send(field)
|
129
|
+
result = compare_values(field, current_val, test_val, 'Bulk JSON')
|
130
|
+
results << result
|
131
|
+
|
132
|
+
if result[:status] == :pass
|
133
|
+
puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
|
134
|
+
else
|
135
|
+
puts " ✗ #{field}: MISMATCH"
|
136
|
+
puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
|
137
|
+
puts " Bulk: #{result[:test][:value]} (#{result[:test][:class]})"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
puts "\n"
|
142
|
+
|
143
|
+
# Test Selective strategy
|
144
|
+
puts 'Strategy: Selective (only JSON-like strings)'
|
145
|
+
puts '-' * 70
|
146
|
+
fields_to_test.each do |field|
|
147
|
+
current_val = current_obj.send(field)
|
148
|
+
test_val = selective_obj.send(field)
|
149
|
+
result = compare_values(field, current_val, test_val, 'Selective')
|
150
|
+
results << result
|
151
|
+
|
152
|
+
if result[:status] == :pass
|
153
|
+
puts " ✓ #{field}: #{test_val.inspect} (#{test_val.class})"
|
154
|
+
else
|
155
|
+
puts " ✗ #{field}: MISMATCH"
|
156
|
+
puts " Current: #{result[:current][:value]} (#{result[:current][:class]})"
|
157
|
+
puts " Selective: #{result[:test][:value]} (#{result[:test][:class]})"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
puts "\n" + ('=' * 70)
|
162
|
+
puts 'SUMMARY'
|
163
|
+
puts '=' * 70
|
164
|
+
|
165
|
+
# Group results by strategy
|
166
|
+
by_strategy = results.group_by { |r| r[:strategy] }
|
167
|
+
|
168
|
+
by_strategy.each do |strategy, strategy_results|
|
169
|
+
passed = strategy_results.count { |r| r[:status] == :pass }
|
170
|
+
failed = strategy_results.count { |r| r[:status] == :fail }
|
171
|
+
total = strategy_results.size
|
172
|
+
|
173
|
+
status_icon = failed == 0 ? '✓' : '✗'
|
174
|
+
puts "\n#{status_icon} #{strategy}: #{passed}/#{total} passed"
|
175
|
+
|
176
|
+
next unless failed > 0
|
177
|
+
|
178
|
+
puts ' Failed fields:'
|
179
|
+
strategy_results.select { |r| r[:status] == :fail }.each do |result|
|
180
|
+
puts " - #{result[:field]}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Overall assessment
|
185
|
+
all_passed = results.all? { |r| r[:status] == :pass }
|
186
|
+
|
187
|
+
puts "\n" + ('=' * 70)
|
188
|
+
puts 'VERDICT'
|
189
|
+
puts '=' * 70
|
190
|
+
|
191
|
+
if all_passed
|
192
|
+
puts '✓ All strategies produce identical results to current implementation'
|
193
|
+
puts ' Safe to use for optimization'
|
194
|
+
else
|
195
|
+
puts '✗ Some strategies produce different results'
|
196
|
+
puts ' Review failures before implementing'
|
197
|
+
end
|
198
|
+
|
199
|
+
puts "\n" + ('=' * 70)
|
200
|
+
puts 'RECOMMENDATIONS'
|
201
|
+
puts '=' * 70
|
202
|
+
|
203
|
+
# Analyze which strategies passed
|
204
|
+
bulk_passed = by_strategy['Bulk JSON'].all? { |r| r[:status] == :pass }
|
205
|
+
selective_passed = by_strategy['Selective'].all? { |r| r[:status] == :pass }
|
206
|
+
|
207
|
+
if bulk_passed && selective_passed
|
208
|
+
puts '✓ Both Bulk JSON and Selective strategies are correct'
|
209
|
+
puts ' → Use Selective for best performance (+15-18% overhead)'
|
210
|
+
puts ' → Use Bulk JSON for simplicity (+20% overhead)'
|
211
|
+
elsif bulk_passed
|
212
|
+
puts '✓ Bulk JSON strategy is correct'
|
213
|
+
puts ' → Safe to implement (+20% overhead vs baseline)'
|
214
|
+
elsif selective_passed
|
215
|
+
puts '✓ Selective strategy is correct'
|
216
|
+
puts ' → Safe to implement (+15-18% overhead vs baseline)'
|
217
|
+
else
|
218
|
+
puts '✗ No alternative strategies passed all tests'
|
219
|
+
puts ' → Stick with current implementation'
|
220
|
+
puts ' → Or fix identified issues in failing strategies'
|
221
|
+
end
|
222
|
+
|
223
|
+
# Cleanup
|
224
|
+
user.destroy!
|
225
|
+
|
226
|
+
__END__
|
227
|
+
|
228
|
+
# Expected output:
|
229
|
+
#
|
230
|
+
# All strategies should pass if they correctly handle:
|
231
|
+
# - Simple strings (no parsing needed)
|
232
|
+
# - JSON objects (hashes)
|
233
|
+
# - JSON arrays
|
234
|
+
# - Numbers as strings
|
235
|
+
# - Empty strings
|
236
|
+
# - Nil values
|
237
|
+
# - Nested JSON structures
|
@@ -6,10 +6,20 @@
|
|
6
6
|
|
7
7
|
require 'digest'
|
8
8
|
|
9
|
-
require_relative '
|
9
|
+
require_relative '../../../lib/familia'
|
10
10
|
|
11
11
|
Familia.enable_database_logging = true
|
12
12
|
Familia.enable_database_counter = true
|
13
|
+
Familia.uri = 'redis://127.0.0.1:2525'
|
14
|
+
|
15
|
+
def generate_random_email
|
16
|
+
# Generate a random username
|
17
|
+
username = (0...8).map { ('a'..'z').to_a[rand(26)] }.join
|
18
|
+
# Define a domain
|
19
|
+
domain = "example.com"
|
20
|
+
# Combine to form an email address
|
21
|
+
"#{username}@#{domain}"
|
22
|
+
end
|
13
23
|
|
14
24
|
class Bone < Familia::Horreum
|
15
25
|
using Familia::Refinements::TimeLiterals
|
@@ -44,12 +54,10 @@ class Customer < Familia::Horreum
|
|
44
54
|
|
45
55
|
using Familia::Refinements::TimeLiterals
|
46
56
|
|
47
|
-
logical_database
|
57
|
+
logical_database 3 # Use something other than the default DB
|
48
58
|
default_expiration 5.years
|
49
59
|
|
50
60
|
feature :safe_dump
|
51
|
-
# feature :expiration
|
52
|
-
# feature :api_version
|
53
61
|
|
54
62
|
# Use new SafeDump DSL instead of @safe_dump_fields
|
55
63
|
safe_dump_field :custid
|
@@ -108,7 +116,7 @@ end
|
|
108
116
|
class Session < Familia::Horreum
|
109
117
|
using Familia::Refinements::TimeLiterals
|
110
118
|
|
111
|
-
logical_database
|
119
|
+
logical_database 2 # a non-default database
|
112
120
|
default_expiration 180.minutes
|
113
121
|
|
114
122
|
identifier_field :sessid
|
@@ -195,7 +203,7 @@ end
|
|
195
203
|
#
|
196
204
|
# NOTE: This will do nothing unless RedactedString is already requried
|
197
205
|
unless defined?(RedactedString)
|
198
|
-
require_relative '
|
206
|
+
require_relative '../../../lib/familia/features/transient_fields/redacted_string'
|
199
207
|
end
|
200
208
|
module RedactedStringTestHelper
|
201
209
|
refine RedactedString do
|
@@ -207,7 +215,7 @@ module RedactedStringTestHelper
|
|
207
215
|
end
|
208
216
|
|
209
217
|
unless defined?(SingleUseRedactedString)
|
210
|
-
require_relative '
|
218
|
+
require_relative '../../../lib/familia/features/transient_fields/single_use_redacted_string'
|
211
219
|
end
|
212
220
|
module SingleUseRedactedStringTestHelper
|
213
221
|
refine SingleUseRedactedString do
|
@@ -59,7 +59,7 @@ docker exec $CONTAINER_ID bash -c '
|
|
59
59
|
# $
|
60
60
|
# $ docker run --rm -d -p 3000:3000 \
|
61
61
|
# -e SECRET=$SECRET \
|
62
|
-
# -e REDIS_URL=redis://host.docker.internal:
|
62
|
+
# -e REDIS_URL=redis://host.docker.internal:2525/0 \
|
63
63
|
# ghcr.io/onetimesecret/devtimesecret-lite:latest
|
64
64
|
#
|
65
65
|
# abcd1234
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/core/base_enhancements_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -97,14 +97,6 @@ EmptyBaseTest.respond_to?(:feature)
|
|
97
97
|
Familia::Base.features_available
|
98
98
|
#=:> Hash
|
99
99
|
|
100
|
-
## Dump and load methods are set
|
101
|
-
Familia::Base.dump_method
|
102
|
-
#=> :to_json
|
103
|
-
|
104
|
-
## Load method is set correctly
|
105
|
-
Familia::Base.load_method
|
106
|
-
#=> :from_json
|
107
|
-
|
108
100
|
## Base module provides inspect with class name
|
109
101
|
@base_uuid.inspect.include?('BaseUuidTest')
|
110
102
|
#=> true
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/core/connection_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -12,10 +12,10 @@ Familia.uri
|
|
12
12
|
|
13
13
|
## Default URI points to localhost database server
|
14
14
|
Familia.uri.to_s
|
15
|
-
#=> "redis://127.0.0.1"
|
15
|
+
#=> "redis://127.0.0.1:2525"
|
16
16
|
|
17
17
|
## Can parse URI from string
|
18
|
-
uri = URI.parse('redis://localhost:
|
18
|
+
uri = URI.parse('redis://localhost:2525/1')
|
19
19
|
uri.host
|
20
20
|
#=> "localhost"
|
21
21
|
|
@@ -29,7 +29,7 @@ Familia.connect
|
|
29
29
|
|
30
30
|
## Can create connection to different URI
|
31
31
|
## Doesn't confirm the logical DB number, dbclient.options raises an error?
|
32
|
-
test_uri = 'redis://localhost:
|
32
|
+
test_uri = 'redis://localhost:2525/2'
|
33
33
|
Familia.create_dbclient(test_uri)
|
34
34
|
#=:> Redis
|
35
35
|
|
@@ -48,7 +48,7 @@ Familia.enable_database_counter
|
|
48
48
|
#=> true
|
49
49
|
|
50
50
|
## Middleware gets registered when enabled
|
51
|
-
dbclient = Familia.create_dbclient('redis://localhost:
|
51
|
+
dbclient = Familia.create_dbclient('redis://localhost:2525/2')
|
52
52
|
dbclient.ping
|
53
53
|
#=> "PONG"
|
54
54
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/core/errors_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
Familia.debug = false
|
6
6
|
|
@@ -40,16 +40,16 @@ raise Familia::NotDistinguishableError, 'A customized message'
|
|
40
40
|
#=~> /A customized message/
|
41
41
|
|
42
42
|
## NotConnected error stores URI
|
43
|
-
test_uri = URI.parse('redis://localhost:
|
43
|
+
test_uri = URI.parse('redis://localhost:2525')
|
44
44
|
begin
|
45
45
|
raise Familia::NotConnected.new(test_uri)
|
46
46
|
rescue Familia::NotConnected => e
|
47
47
|
e.uri.to_s
|
48
48
|
end
|
49
|
-
#=> "redis://localhost"
|
49
|
+
#=> "redis://localhost:2525"
|
50
50
|
|
51
51
|
## NotConnected error has custom message
|
52
|
-
test_uri = URI.parse('redis://localhost:
|
52
|
+
test_uri = URI.parse('redis://localhost:2525')
|
53
53
|
begin
|
54
54
|
raise Familia::NotConnected.new(test_uri)
|
55
55
|
rescue Familia::NotConnected => e
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# try/unit/core/familia_logger_try.rb
|
2
|
+
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
|
+
require 'logger'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
## FamiliaLogger has trace method
|
8
|
+
logger = Familia::FamiliaLogger.new(StringIO.new)
|
9
|
+
logger.respond_to?(:trace)
|
10
|
+
#=> true
|
11
|
+
|
12
|
+
## trace method logs with TRACE level
|
13
|
+
output = StringIO.new
|
14
|
+
logger = Familia::FamiliaLogger.new(output)
|
15
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
16
|
+
logger.trace('Test message')
|
17
|
+
output.string
|
18
|
+
#=~> /Test message/
|
19
|
+
|
20
|
+
## FamiliaLogger has TRACE constant
|
21
|
+
Familia::FamiliaLogger::TRACE
|
22
|
+
#=> 0
|
23
|
+
|
24
|
+
## trace method accepts progname parameter
|
25
|
+
output = StringIO.new
|
26
|
+
logger = Familia::FamiliaLogger.new(output)
|
27
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
28
|
+
logger.trace('MyApp') { 'Test message' }
|
29
|
+
output.string
|
30
|
+
#=~> /MyApp/
|
31
|
+
#=~> /Test message/
|
32
|
+
|
33
|
+
## trace method accepts block for message
|
34
|
+
output = StringIO.new
|
35
|
+
logger = Familia::FamiliaLogger.new(output)
|
36
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
37
|
+
logger.trace { 'Block message' }
|
38
|
+
output.string
|
39
|
+
#=~> /Block message/
|
40
|
+
|
41
|
+
## LogFormatter properly formats TRACE messages
|
42
|
+
output = StringIO.new
|
43
|
+
logger = Familia::FamiliaLogger.new(output)
|
44
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
45
|
+
logger.formatter = Familia::LogFormatter.new
|
46
|
+
logger.trace('Trace test')
|
47
|
+
output.string
|
48
|
+
#=~> /^T,/
|
49
|
+
|
50
|
+
## Nested trace calls preserve TRACE context
|
51
|
+
output = StringIO.new
|
52
|
+
logger = Familia::FamiliaLogger.new(output)
|
53
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
54
|
+
logger.formatter = Familia::LogFormatter.new
|
55
|
+
logger.trace do
|
56
|
+
logger.trace('Inner trace message')
|
57
|
+
'Outer trace message'
|
58
|
+
end
|
59
|
+
output.string.lines.size
|
60
|
+
#=> 2
|
61
|
+
|
62
|
+
## Nested trace inner message formatted as TRACE
|
63
|
+
output = StringIO.new
|
64
|
+
logger = Familia::FamiliaLogger.new(output)
|
65
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
66
|
+
logger.formatter = Familia::LogFormatter.new
|
67
|
+
logger.trace do
|
68
|
+
logger.trace('Inner trace message')
|
69
|
+
'Outer trace message'
|
70
|
+
end
|
71
|
+
output.string.lines[0]
|
72
|
+
#=~> /^T,.*Inner trace message/
|
73
|
+
|
74
|
+
## Nested trace outer message formatted as TRACE
|
75
|
+
output = StringIO.new
|
76
|
+
logger = Familia::FamiliaLogger.new(output)
|
77
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
78
|
+
logger.formatter = Familia::LogFormatter.new
|
79
|
+
logger.trace do
|
80
|
+
logger.trace('Inner trace message')
|
81
|
+
'Outer trace message'
|
82
|
+
end
|
83
|
+
output.string.lines[1]
|
84
|
+
#=~> /^T,.*Outer trace message/
|
85
|
+
|
86
|
+
## Nested trace calls clean up context for subsequent debug calls
|
87
|
+
output = StringIO.new
|
88
|
+
logger = Familia::FamiliaLogger.new(output)
|
89
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
90
|
+
logger.formatter = Familia::LogFormatter.new
|
91
|
+
logger.trace do
|
92
|
+
logger.trace('Inner trace')
|
93
|
+
'Outer trace'
|
94
|
+
end
|
95
|
+
logger.debug('After nested traces')
|
96
|
+
output.string.lines.size
|
97
|
+
#=> 3
|
98
|
+
|
99
|
+
## Debug call after nested traces formatted as DEBUG not TRACE
|
100
|
+
output = StringIO.new
|
101
|
+
logger = Familia::FamiliaLogger.new(output)
|
102
|
+
logger.level = Familia::FamiliaLogger::TRACE
|
103
|
+
logger.formatter = Familia::LogFormatter.new
|
104
|
+
logger.trace do
|
105
|
+
logger.trace('Inner trace')
|
106
|
+
'Outer trace'
|
107
|
+
end
|
108
|
+
logger.debug('After nested traces')
|
109
|
+
output.string.lines[2]
|
110
|
+
#=~> /^D,.*After nested traces/
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# try/core/familia_try.rb
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative '../../support/helpers/test_helpers'
|
4
4
|
|
5
5
|
## Check for help class
|
6
6
|
Bone.related_fields.keys # consistent b/c hashes are ordered
|
@@ -12,7 +12,7 @@ Familia.uri
|
|
12
12
|
|
13
13
|
## Familia has a uri as a string
|
14
14
|
Familia.uri.to_s
|
15
|
-
#=> 'redis://127.0.0.1'
|
15
|
+
#=> 'redis://127.0.0.1:2525'
|
16
16
|
|
17
17
|
## Familia has a url, an alias to uri
|
18
18
|
Familia.url.eql?(Familia.uri)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# Test Valkey/Redis middleware components
|
4
4
|
# Mock Valkey/Redis client with middleware for testing
|
5
5
|
|
6
|
-
require_relative '
|
6
|
+
require_relative '../../support/helpers/test_helpers'
|
7
7
|
|
8
8
|
class MockDatabase
|
9
9
|
attr_reader :logged_commands
|
@@ -27,6 +27,46 @@ class MockDatabase
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
+
## increment_middleware_version! increases version counter
|
31
|
+
initial_version = Familia.middleware_version
|
32
|
+
Familia.increment_middleware_version!
|
33
|
+
Familia.middleware_version > initial_version
|
34
|
+
#=> true
|
35
|
+
|
36
|
+
## increment_middleware_version! increments by exactly 1
|
37
|
+
initial_version = Familia.middleware_version
|
38
|
+
Familia.increment_middleware_version!
|
39
|
+
Familia.middleware_version - initial_version
|
40
|
+
#=> 1
|
41
|
+
|
42
|
+
## fiber_connection= stores connection with current version
|
43
|
+
mock_connection = "test_connection"
|
44
|
+
Familia.fiber_connection=(mock_connection)
|
45
|
+
stored = Fiber[:familia_connection]
|
46
|
+
[stored[0], stored[1] == Familia.middleware_version]
|
47
|
+
#=> ["test_connection", true]
|
48
|
+
|
49
|
+
## fiber_connection= updates version when middleware version changes
|
50
|
+
mock_connection = "test_connection"
|
51
|
+
Familia.fiber_connection=(mock_connection)
|
52
|
+
old_version = Fiber[:familia_connection][1]
|
53
|
+
Familia.increment_middleware_version!
|
54
|
+
Familia.fiber_connection=(mock_connection)
|
55
|
+
new_version = Fiber[:familia_connection][1]
|
56
|
+
new_version > old_version
|
57
|
+
#=> true
|
58
|
+
|
59
|
+
## clear_fiber_connection! removes fiber-local connection
|
60
|
+
Familia.fiber_connection=("test_connection")
|
61
|
+
Familia.clear_fiber_connection!
|
62
|
+
Fiber[:familia_connection]
|
63
|
+
#=> nil
|
64
|
+
|
65
|
+
## clear_fiber_connection! is safe when no connection exists
|
66
|
+
Familia.clear_fiber_connection!
|
67
|
+
Fiber[:familia_connection]
|
68
|
+
#=> nil
|
69
|
+
|
30
70
|
## MockDatabase can log commands with timing
|
31
71
|
dbclient = MockDatabase.new
|
32
72
|
result = dbclient.get("test_key")
|
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
# Test Familia::Tools - key migration and utility functions
|
4
4
|
|
5
|
-
require_relative '
|
5
|
+
require_relative '../../support/helpers/test_helpers'
|
6
6
|
|
7
7
|
## move_keys across Valkey/Redis instances (if available)
|
8
8
|
begin
|
9
|
-
source_redis = Redis.new(db:
|
10
|
-
dest_redis = Redis.new(db:
|
9
|
+
source_redis = Redis.new(db: 1, port: 2525)
|
10
|
+
dest_redis = Redis.new(db: 2, port: 2525)
|
11
11
|
source_redis.set('test:key1', 'value1')
|
12
12
|
source_redis.set('test:key2', 'value2')
|
13
13
|
|
@@ -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)
|