familia 2.0.0.pre15 → 2.0.0.pre16
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/code-quality.yml +138 -0
- data/.github/workflows/code-smellage.yml +145 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +1 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +48 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +64 -4
- data/CLAUDE.md +1 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +41 -41
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +623 -19
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +6 -6
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +49 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +13 -10
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +26 -15
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -5
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +75 -47
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +66 -64
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +9 -12
- data/lib/familia/features/object_identifier.rb +56 -19
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +301 -0
- data/lib/familia/features/relationships/indexing.rb +176 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +160 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +3 -5
- data/lib/familia/features.rb +4 -13
- data/lib/familia/field_type.rb +24 -4
- data/lib/familia/horreum/core/connection.rb +229 -26
- data/lib/familia/horreum/core/database_commands.rb +27 -17
- data/lib/familia/horreum/core/serialization.rb +40 -20
- data/lib/familia/horreum/core/utils.rb +2 -1
- data/lib/familia/horreum/shared/settings.rb +2 -1
- data/lib/familia/horreum/subclass/definition.rb +33 -45
- data/lib/familia/horreum/subclass/management.rb +72 -24
- data/lib/familia/horreum/subclass/related_fields_management.rb +82 -21
- data/lib/familia/horreum.rb +196 -114
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +1 -1
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +7 -7
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +10 -10
- data/try/core/errors_try.rb +8 -11
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +1 -1
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_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 +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +433 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +1 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +3 -3
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +1 -1
- data/try/horreum/destroy_related_fields_cleanup_try.rb +330 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +1 -1
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +8 -10
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- metadata +75 -43
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative '../../lib/familia'
|
4
4
|
|
5
5
|
class MegaCustomer < Familia::Horreum
|
6
|
+
include Familia::Features::Autoloader
|
7
|
+
|
6
8
|
field :custid
|
7
9
|
field :username
|
8
10
|
field :email
|
@@ -13,5 +15,5 @@ class MegaCustomer < Familia::Horreum
|
|
13
15
|
field :updated_at
|
14
16
|
|
15
17
|
feature :safe_dump
|
16
|
-
|
18
|
+
feature :deprecated_fields
|
17
19
|
end
|
@@ -0,0 +1,378 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# examples/encrypted_fields.rb
|
4
|
+
#
|
5
|
+
# Demonstrates the EncryptedFields feature for protecting sensitive data.
|
6
|
+
# This feature provides transparent encryption/decryption of sensitive fields
|
7
|
+
# using strong cryptographic algorithms with field-specific key derivation.
|
8
|
+
|
9
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
10
|
+
require 'familia'
|
11
|
+
|
12
|
+
# Configure connection
|
13
|
+
Familia.uri = 'redis://localhost:6379/15'
|
14
|
+
|
15
|
+
puts '=== Encrypted Fields Feature Examples ==='
|
16
|
+
puts
|
17
|
+
|
18
|
+
# Configure encryption keys for examples
|
19
|
+
Familia.configure do |config|
|
20
|
+
config.encryption_keys = {
|
21
|
+
v1: 'dGVzdGtleWZvcmV4YW1wbGVzMTIzNDU2Nzg5MA==', # Base64 encoded 32 bytes
|
22
|
+
v2: 'bmV3ZXJrZXlmb3JleGFtcGxlczEyMzQ1Njc4OTA=', # Base64 encoded 32 bytes
|
23
|
+
}
|
24
|
+
config.current_key_version = :v2
|
25
|
+
config.encryption_personalization = 'FamiliaExamples'
|
26
|
+
end
|
27
|
+
|
28
|
+
# Validate configuration before proceeding
|
29
|
+
begin
|
30
|
+
Familia::Encryption.validate_configuration!
|
31
|
+
puts '✓ Encryption configuration validated'
|
32
|
+
puts " Algorithm: #{Familia::Encryption.status[:default_algorithm]}"
|
33
|
+
puts " Available algorithms: #{Familia::Encryption.status[:available_algorithms].join(', ')}"
|
34
|
+
puts " Key versions: #{Familia::Encryption.status[:key_versions].join(', ')}"
|
35
|
+
puts
|
36
|
+
rescue Familia::EncryptionError => e
|
37
|
+
puts "✗ Encryption configuration error: #{e.message}"
|
38
|
+
exit 1
|
39
|
+
end
|
40
|
+
|
41
|
+
# Example 1: Basic encrypted fields
|
42
|
+
class SecureUser < Familia::Horreum
|
43
|
+
feature :encrypted_fields
|
44
|
+
|
45
|
+
identifier_field :email
|
46
|
+
field :email # stored as plaintext in the database
|
47
|
+
field :name
|
48
|
+
encrypted_field :ssn # store as an encrypted string in the database
|
49
|
+
encrypted_field :credit_card
|
50
|
+
encrypted_field :notes
|
51
|
+
field :created_at
|
52
|
+
end
|
53
|
+
|
54
|
+
puts 'Example 1: Basic encrypted fields'
|
55
|
+
user = SecureUser.new(
|
56
|
+
email: 'alice@example.com',
|
57
|
+
name: 'Alice Windows',
|
58
|
+
ssn: '123-45-6789',
|
59
|
+
credit_card: '4111-1111-1111-1111',
|
60
|
+
notes: 'VIP customer with special handling',
|
61
|
+
created_at: Familia.now.to_i
|
62
|
+
)
|
63
|
+
|
64
|
+
user.save
|
65
|
+
puts '✓ User saved with encrypted fields'
|
66
|
+
|
67
|
+
# Demonstrate transparent access
|
68
|
+
puts "Name (plaintext): #{user.name}"
|
69
|
+
puts "SSN (encrypted): #{user.ssn.class} -> #{user.ssn.reveal}"
|
70
|
+
puts "Credit card: #{user.credit_card.reveal}"
|
71
|
+
puts "Notes: #{user.notes.reveal}"
|
72
|
+
|
73
|
+
# Show how ConcealedString protects data in logs
|
74
|
+
puts "SSN to_s (safe for logging): #{user.ssn}"
|
75
|
+
puts "SSN inspect (safe for logging): #{user.ssn.inspect}"
|
76
|
+
puts
|
77
|
+
|
78
|
+
# Example 2: Encrypted fields with Additional Authenticated Data (AAD)
|
79
|
+
class SecureDocument < Familia::Horreum
|
80
|
+
feature :encrypted_fields
|
81
|
+
|
82
|
+
identifier_field :doc_id
|
83
|
+
field :doc_id
|
84
|
+
field :title # Plaintext
|
85
|
+
field :owner_id # Plaintext
|
86
|
+
field :classification # Plaintext
|
87
|
+
encrypted_field :content, aad_fields: %i[doc_id owner_id classification]
|
88
|
+
encrypted_field :summary # No AAD
|
89
|
+
field :created_at # Plaintext
|
90
|
+
end
|
91
|
+
|
92
|
+
puts 'Example 2: Encrypted fields with Additional Authenticated Data'
|
93
|
+
doc = SecureDocument.new(
|
94
|
+
doc_id: 'DOC-2024-001',
|
95
|
+
title: 'Strategic Plan',
|
96
|
+
owner_id: 'user123',
|
97
|
+
classification: 'confidential',
|
98
|
+
content: 'This document contains sensitive strategic information...',
|
99
|
+
summary: 'Strategic planning document for Q1 2024',
|
100
|
+
created_at: Familia.now.to_i
|
101
|
+
)
|
102
|
+
|
103
|
+
doc.save
|
104
|
+
puts '✓ Document saved with AAD-protected content'
|
105
|
+
|
106
|
+
# AAD ensures content can only be decrypted with matching metadata
|
107
|
+
puts "Title: #{doc.title}"
|
108
|
+
puts "Content (with AAD protection): #{doc.content.reveal}"
|
109
|
+
puts "Summary (no AAD): #{doc.summary.reveal}"
|
110
|
+
puts
|
111
|
+
|
112
|
+
# Example 3: Performance optimization with request caching
|
113
|
+
class VaultEntry < Familia::Horreum
|
114
|
+
feature :encrypted_fields
|
115
|
+
|
116
|
+
identifier_field :entry_id
|
117
|
+
field :entry_id
|
118
|
+
encrypted_field :api_key
|
119
|
+
encrypted_field :secret_token
|
120
|
+
encrypted_field :private_key
|
121
|
+
encrypted_field :webhook_url
|
122
|
+
end
|
123
|
+
|
124
|
+
puts 'Example 3: Performance optimization with request caching'
|
125
|
+
entries = []
|
126
|
+
|
127
|
+
# Without caching - each field derives keys independently
|
128
|
+
start_time = Time.now
|
129
|
+
5.times do |i|
|
130
|
+
private_key_pem = <<~PEM
|
131
|
+
-----BEGIN PRIVATE KEY-----
|
132
|
+
FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
|
133
|
+
FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
|
134
|
+
-----END PRIVATE KEY-----
|
135
|
+
PEM
|
136
|
+
|
137
|
+
entry = VaultEntry.new(
|
138
|
+
entry_id: "entry_#{i}",
|
139
|
+
api_key: "sk_test_key_#{i}",
|
140
|
+
secret_token: "token_#{i}_secret",
|
141
|
+
private_key: private_key_pem.strip,
|
142
|
+
webhook_url: "https://api.example.com/webhook/#{i}"
|
143
|
+
)
|
144
|
+
entry.save
|
145
|
+
entries << entry
|
146
|
+
end
|
147
|
+
no_cache_time = Time.now - start_time
|
148
|
+
|
149
|
+
# With caching - reuses derived keys within the block
|
150
|
+
start_time = Time.now
|
151
|
+
Familia::Encryption.with_request_cache do
|
152
|
+
5.times do |i|
|
153
|
+
private_key_pem = <<~PEM
|
154
|
+
-----BEGIN PRIVATE KEY-----
|
155
|
+
FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
|
156
|
+
FAKE_EXAMPLE_KEY_FOR_TESTING_ONLY
|
157
|
+
-----END PRIVATE KEY-----
|
158
|
+
PEM
|
159
|
+
|
160
|
+
entry = VaultEntry.new(
|
161
|
+
entry_id: "cached_entry_#{i}",
|
162
|
+
api_key: "sk_test_key_cached_#{i}",
|
163
|
+
secret_token: "token_cached_#{i}_secret",
|
164
|
+
private_key: private_key_pem.strip,
|
165
|
+
webhook_url: "https://api.example.com/webhook/cached/#{i}"
|
166
|
+
)
|
167
|
+
entry.save
|
168
|
+
entries << entry
|
169
|
+
end
|
170
|
+
end
|
171
|
+
cached_time = Time.now - start_time
|
172
|
+
|
173
|
+
puts "Encryption without caching: #{(no_cache_time * 1000).round(2)}ms"
|
174
|
+
puts "Encryption with caching: #{(cached_time * 1000).round(2)}ms"
|
175
|
+
puts "Performance improvement: #{((no_cache_time - cached_time) / no_cache_time * 100).round(1)}%"
|
176
|
+
puts
|
177
|
+
|
178
|
+
# Example 4: Key rotation simulation
|
179
|
+
class RotationTest < Familia::Horreum
|
180
|
+
feature :encrypted_fields
|
181
|
+
|
182
|
+
identifier_field :test_id
|
183
|
+
field :test_id
|
184
|
+
encrypted_field :sensitive_data
|
185
|
+
end
|
186
|
+
|
187
|
+
puts 'Example 4: Key rotation demonstration'
|
188
|
+
rotation_obj = RotationTest.new(
|
189
|
+
test_id: 'rotation_test',
|
190
|
+
sensitive_data: 'Original sensitive data encrypted with v2 key'
|
191
|
+
)
|
192
|
+
rotation_obj.save
|
193
|
+
|
194
|
+
puts "Original data encrypted with key version: #{Familia.config.current_key_version}"
|
195
|
+
puts "Data: #{rotation_obj.sensitive_data.reveal}"
|
196
|
+
|
197
|
+
# Check encryption status before rotation
|
198
|
+
status_before = rotation_obj.encrypted_fields_status
|
199
|
+
puts "Encryption status before rotation: #{status_before}"
|
200
|
+
|
201
|
+
# Simulate key rotation - switch to v1 for demonstration
|
202
|
+
Familia.config.current_key_version = :v1
|
203
|
+
|
204
|
+
# Re-encrypt with new current key
|
205
|
+
rotation_obj.sensitive_data = 'Updated data encrypted with v1 key'
|
206
|
+
rotation_obj.re_encrypt_fields!
|
207
|
+
rotation_obj.save
|
208
|
+
|
209
|
+
puts "After rotation to key version: #{Familia.config.current_key_version}"
|
210
|
+
puts "Data: #{rotation_obj.sensitive_data.reveal}"
|
211
|
+
|
212
|
+
# Check encryption status after rotation
|
213
|
+
status_after = rotation_obj.encrypted_fields_status
|
214
|
+
puts "Encryption status after rotation: #{status_after}"
|
215
|
+
|
216
|
+
# Switch back to v2
|
217
|
+
Familia.config.current_key_version = :v2
|
218
|
+
puts
|
219
|
+
|
220
|
+
# Example 5: Memory safety and cleanup
|
221
|
+
class MemoryTest < Familia::Horreum
|
222
|
+
feature :encrypted_fields
|
223
|
+
|
224
|
+
identifier_field :mem_id
|
225
|
+
field :mem_id
|
226
|
+
encrypted_field :secret_one
|
227
|
+
encrypted_field :secret_two
|
228
|
+
encrypted_field :secret_three
|
229
|
+
end
|
230
|
+
|
231
|
+
puts 'Example 5: Memory safety and cleanup'
|
232
|
+
mem_obj = MemoryTest.new(
|
233
|
+
mem_id: 'memory_test',
|
234
|
+
secret_one: 'First secret value',
|
235
|
+
secret_two: 'Second secret value',
|
236
|
+
secret_three: 'Third secret value'
|
237
|
+
)
|
238
|
+
|
239
|
+
puts "Has encrypted data: #{mem_obj.encrypted_data?}"
|
240
|
+
puts "Fields cleared: #{mem_obj.encrypted_fields_cleared?}"
|
241
|
+
|
242
|
+
# Access some fields to load them into memory
|
243
|
+
puts "Secret one: #{mem_obj.secret_one.reveal}"
|
244
|
+
puts "Secret two: #{mem_obj.secret_two.reveal}"
|
245
|
+
|
246
|
+
# Clear specific field
|
247
|
+
mem_obj.secret_one.clear!
|
248
|
+
puts "Secret one cleared: #{mem_obj.secret_one.cleared?}"
|
249
|
+
|
250
|
+
# Clear all encrypted fields
|
251
|
+
mem_obj.clear_encrypted_fields!
|
252
|
+
puts "All fields cleared: #{mem_obj.encrypted_fields_cleared?}"
|
253
|
+
puts
|
254
|
+
|
255
|
+
# Example 6: Error handling and validation
|
256
|
+
puts 'Example 6: Error handling and validation'
|
257
|
+
|
258
|
+
# Test with invalid configuration
|
259
|
+
begin
|
260
|
+
old_keys = Familia.config.encryption_keys
|
261
|
+
Familia.config.encryption_keys = {}
|
262
|
+
|
263
|
+
invalid_obj = SecureUser.new(email: 'test@example.com', ssn: 'test')
|
264
|
+
invalid_obj.save
|
265
|
+
rescue Familia::EncryptionError => e
|
266
|
+
puts "✓ Caught expected error with invalid config: #{e.message}"
|
267
|
+
ensure
|
268
|
+
Familia.config.encryption_keys = old_keys
|
269
|
+
end
|
270
|
+
|
271
|
+
# Test with missing key version
|
272
|
+
begin
|
273
|
+
test_obj = SecureUser.new(email: 'version_test@example.com', ssn: '987-65-4321')
|
274
|
+
test_obj.save
|
275
|
+
|
276
|
+
# Simulate missing key version in config
|
277
|
+
old_keys = Familia.config.encryption_keys.dup
|
278
|
+
Familia.config.encryption_keys = { v3: old_keys[:v2] }
|
279
|
+
|
280
|
+
# This should fail when trying to decrypt
|
281
|
+
test_obj.ssn.reveal
|
282
|
+
rescue Familia::EncryptionError => e
|
283
|
+
puts "✓ Caught expected error with missing key version: #{e.message}"
|
284
|
+
ensure
|
285
|
+
Familia.config.encryption_keys = old_keys
|
286
|
+
end
|
287
|
+
puts
|
288
|
+
|
289
|
+
# Example 7: Benchmarking encryption performance
|
290
|
+
puts 'Example 7: Encryption performance benchmarks'
|
291
|
+
if defined?(Familia::Encryption) && Familia::Encryption.respond_to?(:benchmark)
|
292
|
+
benchmark_results = Familia::Encryption.benchmark(iterations: 100)
|
293
|
+
|
294
|
+
puts 'Encryption benchmark results (100 iterations):'
|
295
|
+
benchmark_results.each do |algorithm, stats|
|
296
|
+
puts " #{algorithm}:"
|
297
|
+
puts " Time: #{(stats[:time] * 1000).round(2)}ms total"
|
298
|
+
puts " Operations/sec: #{stats[:ops_per_sec]}"
|
299
|
+
puts " Priority: #{stats[:priority]}"
|
300
|
+
end
|
301
|
+
else
|
302
|
+
puts 'Benchmarking not available in this version'
|
303
|
+
end
|
304
|
+
puts
|
305
|
+
|
306
|
+
# Example 8: Integration with safe dump feature
|
307
|
+
class SecureProfile < Familia::Horreum
|
308
|
+
feature :encrypted_fields
|
309
|
+
feature :safe_dump
|
310
|
+
|
311
|
+
identifier_field :profile_id
|
312
|
+
field :profile_id
|
313
|
+
field :username # Safe to expose
|
314
|
+
field :email # Safe to expose
|
315
|
+
encrypted_field :phone # Encrypted but can be safely exposed
|
316
|
+
encrypted_field :ssn # Encrypted and should NOT be exposed
|
317
|
+
encrypted_field :bank_account # Encrypted and should NOT be exposed
|
318
|
+
field :created_at # Safe to expose
|
319
|
+
|
320
|
+
# Define safe dump fields - encrypted fields are handled automatically
|
321
|
+
safe_dump_field :profile_id
|
322
|
+
safe_dump_field :username
|
323
|
+
safe_dump_field :email
|
324
|
+
safe_dump_field :phone_display, lambda { |profile|
|
325
|
+
phone = profile.phone.reveal
|
326
|
+
phone ? "#{phone[0..2]}-***-#{phone[-4..]}" : nil
|
327
|
+
}
|
328
|
+
safe_dump_field :created_at
|
329
|
+
end
|
330
|
+
|
331
|
+
puts 'Example 8: Integration with SafeDump feature'
|
332
|
+
profile = SecureProfile.new(
|
333
|
+
profile_id: 'profile_123',
|
334
|
+
username: 'alice_windows',
|
335
|
+
email: 'alice@example.com',
|
336
|
+
phone: '555-123-4567',
|
337
|
+
ssn: '123-45-6789',
|
338
|
+
bank_account: '9876543210',
|
339
|
+
created_at: Familia.now.to_i
|
340
|
+
)
|
341
|
+
profile.save
|
342
|
+
|
343
|
+
puts 'Profile safe dump (encrypted fields handled automatically):'
|
344
|
+
puts JSON.pretty_generate(profile.safe_dump)
|
345
|
+
puts 'Notice: SSN and bank account are automatically excluded'
|
346
|
+
puts 'Phone number is included but masked for display'
|
347
|
+
puts
|
348
|
+
|
349
|
+
# Clean up examples
|
350
|
+
puts '=== Cleaning up test data ==='
|
351
|
+
[SecureUser, SecureDocument, VaultEntry, RotationTest, MemoryTest, SecureProfile].each do |klass|
|
352
|
+
keys = klass.dbclient.keys("#{klass.name.downcase.gsub('::', '_')}:*")
|
353
|
+
klass.dbclient.del(*keys) unless keys.empty?
|
354
|
+
puts "✓ Cleaned #{klass.name} (#{keys.length} keys)"
|
355
|
+
rescue StandardError => e
|
356
|
+
puts "✗ Error cleaning #{klass.name}: #{e.message}"
|
357
|
+
end
|
358
|
+
|
359
|
+
# Clear any request cache
|
360
|
+
begin
|
361
|
+
Familia::Encryption.clear_request_cache!
|
362
|
+
rescue StandardError => e
|
363
|
+
puts "⚠ Warning: Failed to clear encryption request cache: #{e.message} (#{e.backtrace.join("\n")})"
|
364
|
+
end
|
365
|
+
|
366
|
+
puts
|
367
|
+
puts '=== Summary ==='
|
368
|
+
puts 'This example demonstrated:'
|
369
|
+
puts '• Basic encrypted field usage with transparent access'
|
370
|
+
puts '• Additional Authenticated Data (AAD) for tamper detection'
|
371
|
+
puts '• Performance optimization with request-level caching'
|
372
|
+
puts '• Key rotation procedures and status monitoring'
|
373
|
+
puts '• Memory safety with ConcealedString and cleanup methods'
|
374
|
+
puts '• Error handling for configuration and key version issues'
|
375
|
+
puts '• Encryption performance benchmarking'
|
376
|
+
puts '• Integration with SafeDump for API-safe serialization'
|
377
|
+
puts
|
378
|
+
puts 'Encrypted Fields examples completed!'
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# examples/json_usage_patterns.rb
|
2
|
+
#
|
3
|
+
# This file demonstrates the JSON serialization patterns available in Familia,
|
4
|
+
# showing both the secure defaults and optional developer convenience features.
|
5
|
+
|
6
|
+
require_relative '../lib/familia'
|
7
|
+
|
8
|
+
# Example model setup
|
9
|
+
class User < Familia::Horreum
|
10
|
+
feature :encrypted_fields
|
11
|
+
identifier_field :user_id
|
12
|
+
field :user_id
|
13
|
+
field :name
|
14
|
+
field :email
|
15
|
+
encrypted_field :password_hash # This will be concealed in JSON
|
16
|
+
listkey :tags # Associates a separate ListKey field
|
17
|
+
set :permissions
|
18
|
+
end
|
19
|
+
|
20
|
+
# Configure encryption (required for encrypted_fields)
|
21
|
+
Familia.config.encryption_keys = { v1: Base64.strict_encode64('a' * 32) }
|
22
|
+
Familia.config.current_key_version = :v1
|
23
|
+
|
24
|
+
# Create and save a user
|
25
|
+
user = User.new(
|
26
|
+
user_id: 'user123',
|
27
|
+
name: 'Alice Johnson',
|
28
|
+
email: 'alice@example.com',
|
29
|
+
password_hash: 'hashed_password_secret'
|
30
|
+
)
|
31
|
+
user.save
|
32
|
+
|
33
|
+
user.tags << 'admin' << 'developer'
|
34
|
+
user.permissions << 'read' << 'write'
|
35
|
+
|
36
|
+
puts "=== Familia JSON Serialization Patterns ==="
|
37
|
+
puts
|
38
|
+
|
39
|
+
# Pattern 1: Direct object serialization (always available)
|
40
|
+
puts "1. Direct Familia Object Serialization:"
|
41
|
+
puts " user.as_json # Secure - only public fields"
|
42
|
+
p user.as_json
|
43
|
+
puts " user.to_json # Uses Familia::JsonSerializer (OJ strict mode)"
|
44
|
+
puts user.to_json
|
45
|
+
puts
|
46
|
+
|
47
|
+
puts " user.tags.as_json # DataType objects work too"
|
48
|
+
p user.tags.as_json
|
49
|
+
puts " user.tags.to_json"
|
50
|
+
puts user.tags.to_json
|
51
|
+
puts
|
52
|
+
|
53
|
+
# Pattern 2: Manual mixed serialization (current secure pattern)
|
54
|
+
puts "2. Manual Mixed Serialization (Current Pattern):"
|
55
|
+
mixed_data = {
|
56
|
+
user: user.as_json,
|
57
|
+
tags: user.tags.as_json,
|
58
|
+
permissions: user.permissions.as_json,
|
59
|
+
meta: { timestamp: Time.now.to_i }
|
60
|
+
}
|
61
|
+
puts " Manual preparation + JsonSerializer.dump:"
|
62
|
+
puts Familia::JsonSerializer.dump(mixed_data)
|
63
|
+
puts
|
64
|
+
|
65
|
+
# Pattern 3: Opt-in refinement for Hash/Array (new convenience feature)
|
66
|
+
puts "3. Opt-in Refinement Pattern (Developer Convenience):"
|
67
|
+
puts " # Add this line to enable refinements in your file:"
|
68
|
+
puts " using Familia::Refinements::DearJson"
|
69
|
+
puts
|
70
|
+
|
71
|
+
# Demonstrate the refinement
|
72
|
+
require_relative '../lib/familia/refinements/dear_json'
|
73
|
+
using Familia::Refinements::DearJson
|
74
|
+
|
75
|
+
mixed_hash = {
|
76
|
+
user: user, # Familia object (will call as_json)
|
77
|
+
tags: user.tags, # Familia DataType (will call as_json)
|
78
|
+
meta: { timestamp: Time.now.to_i } # Plain hash (passes through)
|
79
|
+
}
|
80
|
+
|
81
|
+
mixed_array = [
|
82
|
+
user, # Familia object
|
83
|
+
user.tags, # Familia DataType
|
84
|
+
{ type: 'example' }, # Plain hash
|
85
|
+
'metadata' # Plain string
|
86
|
+
]
|
87
|
+
|
88
|
+
puts " # Now Hash and Array have secure to_json using Familia::JsonSerializer"
|
89
|
+
puts " mixed_hash.to_json:"
|
90
|
+
puts mixed_hash.to_json
|
91
|
+
puts
|
92
|
+
|
93
|
+
puts " mixed_array.to_json:"
|
94
|
+
puts mixed_array.to_json
|
95
|
+
puts
|
96
|
+
|
97
|
+
# Pattern 4: Security demonstration
|
98
|
+
puts "4. Security Features (Always Active):"
|
99
|
+
puts " # Encrypted fields are automatically concealed"
|
100
|
+
puts " # This is what user.password_hash looks like:"
|
101
|
+
puts " user.password_hash.class # => #{user.password_hash.class}"
|
102
|
+
puts " user.password_hash.to_s # => #{user.password_hash.to_s}"
|
103
|
+
puts
|
104
|
+
|
105
|
+
begin
|
106
|
+
user.password_hash.to_json
|
107
|
+
rescue Familia::SerializerError => e
|
108
|
+
puts " user.password_hash.to_json # => #{e.class}: #{e.message}"
|
109
|
+
end
|
110
|
+
puts
|
111
|
+
|
112
|
+
puts " # Only public fields appear in JSON (password_hash is excluded):"
|
113
|
+
puts " user.as_json.keys # => #{user.as_json.keys}"
|
114
|
+
puts
|
115
|
+
|
116
|
+
puts "5. Framework Integration Examples:"
|
117
|
+
puts " # Rails controller"
|
118
|
+
puts " def show"
|
119
|
+
puts " render json: user # Works with as_json/to_json"
|
120
|
+
puts " end"
|
121
|
+
puts
|
122
|
+
puts " # Sinatra/Roda response"
|
123
|
+
puts " get '/user/:id' do"
|
124
|
+
puts " content_type :json"
|
125
|
+
puts " user.to_json # Direct serialization"
|
126
|
+
puts " end"
|
127
|
+
puts
|
128
|
+
puts " # API response with refinement"
|
129
|
+
puts " using Familia::Refinements::DearJson"
|
130
|
+
puts " response = {"
|
131
|
+
puts " user: user,"
|
132
|
+
puts " meta: { version: '1.0' }"
|
133
|
+
puts " }"
|
134
|
+
puts " response.to_json # Handles mixed Familia/core objects"
|
135
|
+
puts
|
136
|
+
|
137
|
+
puts "=== Summary ==="
|
138
|
+
puts "✅ Security: All JSON serialization uses OJ strict mode"
|
139
|
+
puts "✅ Encrypted fields: Automatically concealed (ConcealedString protection)"
|
140
|
+
puts "✅ Public fields only: Horreum objects expose only defined fields"
|
141
|
+
puts "✅ DataType support: Lists, sets, etc. serialize their contents"
|
142
|
+
puts "✅ Developer experience: Standard Ruby JSON interface (as_json/to_json)"
|
143
|
+
puts "✅ Opt-in convenience: Refinements for Hash/Array when desired"
|
144
|
+
puts "✅ Framework compatible: Works with Rails, Sinatra, Roda, etc."
|
data/examples/relationships.rb
CHANGED
@@ -27,7 +27,7 @@ class Customer < Familia::Horreum
|
|
27
27
|
|
28
28
|
# Define collections for tracking relationships
|
29
29
|
set :domains # Simple set of domain IDs
|
30
|
-
|
30
|
+
listkey :projects # Ordered list of project IDs
|
31
31
|
sorted_set :activity # Activity feed with timestamps
|
32
32
|
|
33
33
|
# Create indexes for fast lookups (using class_ prefix for class-level)
|
@@ -35,7 +35,7 @@ class Customer < Familia::Horreum
|
|
35
35
|
class_indexed_by :plan, :plan_lookup # i.e. Customer.plan_lookup
|
36
36
|
|
37
37
|
# Track in class-level collections (using class_ prefix for class-level)
|
38
|
-
|
38
|
+
class_participates_in :all_customers, score: :created_at # i.e. Customer.all_customers
|
39
39
|
end
|
40
40
|
|
41
41
|
class Domain < Familia::Horreum
|
@@ -49,11 +49,11 @@ class Domain < Familia::Horreum
|
|
49
49
|
field :status
|
50
50
|
|
51
51
|
# Declare membership in customer collections
|
52
|
-
|
52
|
+
participates_in Customer, :domains # , type: :set
|
53
53
|
|
54
54
|
# Track domains by status (using class_ prefix for class-level)
|
55
|
-
|
56
|
-
|
55
|
+
class_participates_in :active_domains,
|
56
|
+
score: -> { status == 'active' ? Familia.now.to_i : 0 }
|
57
57
|
end
|
58
58
|
|
59
59
|
class Project < Familia::Horreum
|
@@ -66,7 +66,7 @@ class Project < Familia::Horreum
|
|
66
66
|
field :priority
|
67
67
|
|
68
68
|
# Member of customer projects list (ordered)
|
69
|
-
|
69
|
+
participates_in Customer, :projects, type: :list
|
70
70
|
end
|
71
71
|
|
72
72
|
puts '=== 1. Basic Object Creation ==='
|
@@ -110,7 +110,7 @@ puts '=== 2. Establishing Relationships ==='
|
|
110
110
|
customer.save # Automatically adds to email_lookup, plan_lookup, and all_customers
|
111
111
|
puts '✓ Customer automatically added to indexes and tracking on save'
|
112
112
|
|
113
|
-
# Establish
|
113
|
+
# Establish participates_in relationships using clean << operator syntax
|
114
114
|
customer.domains << domain1 # Clean Ruby-like syntax
|
115
115
|
customer.domains << domain2 # Same as domain1.add_to_customer_domains(customer)
|
116
116
|
customer.projects << project # Same as project.add_to_customer_projects(customer)
|
@@ -149,7 +149,7 @@ puts " Customer has #{customer.projects.size} projects"
|
|
149
149
|
puts " Domain IDs: #{customer.domains.members}"
|
150
150
|
puts " Project IDs: #{customer.projects.members}"
|
151
151
|
|
152
|
-
# Test
|
152
|
+
# Test participates_in collections
|
153
153
|
all_customers_count = Customer.values.size
|
154
154
|
puts "\nClass-level tracking:"
|
155
155
|
puts " Total customers in system: #{all_customers_count}"
|
@@ -161,7 +161,7 @@ puts
|
|
161
161
|
puts '=== 4. Range Queries ==='
|
162
162
|
|
163
163
|
# Get recent customers (last 24 hours)
|
164
|
-
yesterday = (
|
164
|
+
yesterday = (Familia.now - (24 * 3600)).to_i # 24 hours ago
|
165
165
|
recent_customers = Customer.values.rangebyscore(yesterday, '+inf')
|
166
166
|
puts "Recent customers (last 24h): #{recent_customers.size}"
|
167
167
|
|
@@ -178,7 +178,7 @@ puts '=== 6. Relationship Cleanup ==='
|
|
178
178
|
# Remove relationships
|
179
179
|
puts 'Cleaning up relationships...'
|
180
180
|
|
181
|
-
# Remove from
|
181
|
+
# Remove from participates_in relationships
|
182
182
|
domain1.remove_from_customer_domains(customer)
|
183
183
|
puts "✓ Removed #{domain1.name} from customer domains"
|
184
184
|
|
@@ -195,10 +195,10 @@ puts
|
|
195
195
|
puts '=== Example Complete! ==='
|
196
196
|
puts
|
197
197
|
puts 'Key takeaways:'
|
198
|
-
puts '•
|
198
|
+
puts '• class_participates_in: Automatic class-level collections updated on save'
|
199
199
|
puts '• class_indexed_by: Automatic class-level indexes updated on save'
|
200
|
-
puts '•
|
201
|
-
puts '• indexed_by with
|
200
|
+
puts '• participates_in: Use << operator for clean Ruby-like collection syntax'
|
201
|
+
puts '• indexed_by with context:: Use for relationship-scoped indexes'
|
202
202
|
puts '• Save operations: Automatically update indexes and class-level tracking'
|
203
203
|
puts '• << operator: Works naturally with all collection types (sets, lists, sorted sets)'
|
204
204
|
puts
|
data/examples/safe_dump.rb
CHANGED
@@ -38,10 +38,10 @@ puts 'Example 1: Basic SafeDump'
|
|
38
38
|
user = User.new(
|
39
39
|
email: 'alice@example.com',
|
40
40
|
first_name: 'Alice',
|
41
|
-
last_name: '
|
41
|
+
last_name: 'Windows',
|
42
42
|
password_hash: 'secret123',
|
43
43
|
ssn: '123-45-6789',
|
44
|
-
created_at:
|
44
|
+
created_at: Familia.now.to_i
|
45
45
|
)
|
46
46
|
|
47
47
|
puts "Full object data: #{user.to_h}"
|
@@ -70,7 +70,7 @@ class Product < Familia::Horreum
|
|
70
70
|
|
71
71
|
# Computed fields using callables
|
72
72
|
safe_dump_field :price, ->(product) { "$#{format('%.2f', product.price_cents.to_i / 100.0)}" }
|
73
|
-
safe_dump_field :in_stock, ->(product) { product.inventory_count.to_i
|
73
|
+
safe_dump_field :in_stock, ->(product) { product.inventory_count.to_i.positive? }
|
74
74
|
safe_dump_field :display_name, ->(product) { "#{product.name} (#{product.sku})" }
|
75
75
|
end
|
76
76
|
|
@@ -82,7 +82,7 @@ product = Product.new(
|
|
82
82
|
cost_cents: 800, # $8.00 - sensitive, not exposed
|
83
83
|
inventory_count: 25,
|
84
84
|
category: 'widgets',
|
85
|
-
created_at:
|
85
|
+
created_at: Familia.now.to_i
|
86
86
|
)
|
87
87
|
|
88
88
|
puts "Full object data: #{product.to_h}"
|
@@ -140,8 +140,8 @@ order = Order.new(
|
|
140
140
|
payment_method: 'credit_card',
|
141
141
|
credit_card_number: '4111-1111-1111-1111', # Never expose this!
|
142
142
|
processing_notes: 'Rush order - expedite shipping',
|
143
|
-
created_at:
|
144
|
-
shipped_at:
|
143
|
+
created_at: Familia.now.to_i - 86_400, # Yesterday
|
144
|
+
shipped_at: Familia.now.to_i - 3600 # 1 hour ago
|
145
145
|
)
|
146
146
|
|
147
147
|
puts "Full object data: #{order.to_h}"
|