familia 2.0.0.pre3 → 2.0.0.pre5
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/.gitignore +3 -0
- data/.rubocop_todo.yml +17 -17
- data/CLAUDE.md +3 -3
- data/Gemfile +5 -1
- data/Gemfile.lock +18 -3
- data/README.md +36 -157
- data/TEST_COVERAGE.md +40 -0
- data/docs/overview.md +359 -0
- data/docs/wiki/API-Reference.md +270 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
- data/docs/wiki/Home.md +49 -0
- data/docs/wiki/Implementation-Guide.md +183 -0
- data/docs/wiki/Security-Model.md +143 -0
- data/lib/familia/base.rb +18 -27
- data/lib/familia/connection.rb +6 -5
- data/lib/familia/{datatype → data_type}/commands.rb +2 -5
- data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
- data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
- data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
- data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
- data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
- data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
- data/lib/familia/{datatype.rb → data_type.rb} +10 -12
- data/lib/familia/encryption/manager.rb +102 -0
- data/lib/familia/encryption/provider.rb +49 -0
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
- data/lib/familia/encryption/registry.rb +50 -0
- data/lib/familia/encryption.rb +178 -0
- data/lib/familia/encryption_request_cache.rb +68 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
- data/lib/familia/features/encrypted_fields.rb +28 -0
- data/lib/familia/features/expiration.rb +107 -77
- data/lib/familia/features/quantization.rb +5 -9
- data/lib/familia/features/relatable_objects.rb +2 -4
- data/lib/familia/features/safe_dump.rb +14 -17
- data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
- data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
- data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
- data/lib/familia/features/transient_fields.rb +47 -0
- data/lib/familia/features.rb +40 -24
- data/lib/familia/field_type.rb +270 -0
- data/lib/familia/horreum/connection.rb +8 -11
- data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
- data/lib/familia/horreum/definition_methods.rb +453 -0
- data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
- data/lib/familia/horreum/serialization.rb +46 -18
- data/lib/familia/horreum/settings.rb +10 -2
- data/lib/familia/horreum/utils.rb +9 -10
- data/lib/familia/horreum.rb +18 -10
- data/lib/familia/logging.rb +14 -14
- data/lib/familia/settings.rb +39 -3
- data/lib/familia/utils.rb +45 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +2 -1
- data/try/core/base_enhancements_try.rb +115 -0
- data/try/core/connection_try.rb +0 -1
- data/try/core/errors_try.rb +0 -1
- data/try/core/familia_extended_try.rb +3 -4
- data/try/core/familia_try.rb +0 -1
- data/try/core/pools_try.rb +2 -2
- data/try/core/secure_identifier_try.rb +0 -1
- data/try/core/settings_try.rb +0 -1
- data/try/core/utils_try.rb +0 -1
- data/try/{datatypes → data_types}/boolean_try.rb +1 -2
- data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
- data/try/{datatypes → data_types}/hash_try.rb +1 -2
- data/try/{datatypes → data_types}/list_try.rb +1 -2
- data/try/{datatypes → data_types}/set_try.rb +1 -2
- data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
- data/try/{datatypes → data_types}/string_try.rb +1 -2
- data/try/debugging/README.md +32 -0
- data/try/debugging/cache_behavior_tracer.rb +91 -0
- data/try/debugging/encryption_method_tracer.rb +138 -0
- data/try/debugging/provider_diagnostics.rb +110 -0
- data/try/edge_cases/hash_symbolization_try.rb +0 -1
- data/try/edge_cases/json_serialization_try.rb +0 -1
- data/try/edge_cases/reserved_keywords_try.rb +42 -11
- data/try/encryption/config_persistence_try.rb +192 -0
- data/try/encryption/encryption_core_try.rb +328 -0
- data/try/encryption/instance_variable_scope_try.rb +31 -0
- data/try/encryption/module_loading_try.rb +28 -0
- data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
- data/try/encryption/roundtrip_validation_try.rb +28 -0
- data/try/encryption/secure_memory_handling_try.rb +125 -0
- data/try/features/encrypted_fields_core_try.rb +117 -0
- data/try/features/encrypted_fields_integration_try.rb +220 -0
- data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
- data/try/features/encrypted_fields_security_try.rb +370 -0
- data/try/features/encryption_fields/aad_protection_try.rb +53 -0
- data/try/features/encryption_fields/context_isolation_try.rb +120 -0
- data/try/features/encryption_fields/error_conditions_try.rb +116 -0
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
- data/try/features/encryption_fields/fresh_key_try.rb +163 -0
- data/try/features/encryption_fields/key_rotation_try.rb +117 -0
- data/try/features/encryption_fields/memory_security_try.rb +37 -0
- data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
- data/try/features/encryption_fields/thread_safety_try.rb +199 -0
- data/try/features/expiration_try.rb +0 -1
- data/try/features/feature_dependencies_try.rb +159 -0
- data/try/features/quantization_try.rb +0 -1
- data/try/features/real_feature_integration_try.rb +148 -0
- data/try/features/relatable_objects_try.rb +0 -1
- data/try/features/safe_dump_advanced_try.rb +0 -1
- data/try/features/safe_dump_try.rb +0 -1
- data/try/features/transient_fields/redacted_string_try.rb +248 -0
- data/try/features/transient_fields/refresh_reset_try.rb +164 -0
- data/try/features/transient_fields/simple_refresh_test.rb +50 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
- data/try/features/transient_fields_core_try.rb +181 -0
- data/try/features/transient_fields_integration_try.rb +260 -0
- data/try/helpers/test_helpers.rb +42 -0
- data/try/horreum/base_try.rb +157 -3
- data/try/horreum/class_methods_try.rb +27 -36
- data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
- data/try/horreum/field_categories_try.rb +118 -0
- data/try/horreum/field_definition_try.rb +96 -0
- data/try/horreum/initialization_try.rb +0 -1
- data/try/horreum/relations_try.rb +0 -1
- data/try/horreum/serialization_persistent_fields_try.rb +165 -0
- data/try/horreum/serialization_try.rb +2 -3
- data/try/memory/memory_basic_test.rb +73 -0
- data/try/memory/memory_detailed_test.rb +121 -0
- data/try/memory/memory_docker_ruby_dump.sh +80 -0
- data/try/memory/memory_search_for_string.rb +83 -0
- data/try/memory/test_actual_redactedstring_protection.rb +38 -0
- data/try/models/customer_safe_dump_try.rb +0 -1
- data/try/models/customer_try.rb +0 -1
- data/try/models/datatype_base_try.rb +1 -2
- data/try/models/familia_object_try.rb +0 -1
- metadata +85 -18
data/docs/wiki/Home.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Familia Encrypted Fields Documentation
|
2
|
+
|
3
|
+
Welcome to the Familia encrypted fields feature documentation. This Wiki provides comprehensive guides for implementing field-level encryption in your Familia-based applications.
|
4
|
+
|
5
|
+
## 📚 Documentation Structure
|
6
|
+
|
7
|
+
### Essential Reading (Start Here)
|
8
|
+
|
9
|
+
1. **[Encrypted Fields Overview](Encrypted-Fields-Overview.md)** - Quick introduction and basic usage
|
10
|
+
|
11
|
+
2. **[Implementation Guide](Implementation-Guide.md)** - Configuration details and advanced usage
|
12
|
+
|
13
|
+
3. **[API Reference](API-Reference.md)** - Class and method documentation
|
14
|
+
|
15
|
+
### Deep Dives
|
16
|
+
|
17
|
+
4. **[Security Model](Security-Model.md)** - Cryptographic design and Protected vs unprotected scenarios
|
18
|
+
|
19
|
+
### Operations (As Needed)
|
20
|
+
|
21
|
+
5. **[Migration Guide](Migration-Guide.md)** - Upgrading existing fields _(coming soon)_
|
22
|
+
6. **[Key Management](Key-Management.md)** - Rotation and best practices _(coming soon)_
|
23
|
+
|
24
|
+
## 🚀 Quick Start
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
# 1. Add encrypted field to your model
|
28
|
+
class User < Familia::Horreum
|
29
|
+
encrypted_field :secret_recipe
|
30
|
+
end
|
31
|
+
|
32
|
+
# 2. Configure encryption key
|
33
|
+
Familia.configure do |config|
|
34
|
+
config.encryption_keys = { v1: ENV['FAMILIA_ENCRYPTION_KEY'] }
|
35
|
+
config.current_key_version = :v1
|
36
|
+
end
|
37
|
+
|
38
|
+
# 3. Use like any other field
|
39
|
+
user = User.new(secret_recipe: "donna's cookies")
|
40
|
+
user.save
|
41
|
+
user.secret_recipe # => "donna's cookies" (automatically decrypted)
|
42
|
+
```
|
43
|
+
|
44
|
+
|
45
|
+
## Related Resources
|
46
|
+
|
47
|
+
- [Familia README](https://github.com/delano/familia) - Main project documentation
|
48
|
+
- [Issue #57](https://github.com/delano/familia/issues/57) - Original feature proposal
|
49
|
+
- [Issue #58](https://github.com/delano/familia/issues/58) - Wiki documentation tracking
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# Implementation Guide
|
2
|
+
|
3
|
+
## Architecture Overview
|
4
|
+
|
5
|
+
The encrypted fields feature extends Familia's existing field system with transformation hooks:
|
6
|
+
|
7
|
+
```
|
8
|
+
User Input → Field Setter → Serialize Transform → Encryption → Redis
|
9
|
+
Redis → Decryption → Deserialize Transform → Field Getter → User Output
|
10
|
+
```
|
11
|
+
|
12
|
+
## Core Components
|
13
|
+
|
14
|
+
### 1. Transform Hooks
|
15
|
+
|
16
|
+
Fields now support transform callbacks:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
FieldDefinition = Data.define(
|
20
|
+
:field_name,
|
21
|
+
:method_name,
|
22
|
+
:serialize_transform, # Called before storage
|
23
|
+
:deserialize_transform # Called after retrieval
|
24
|
+
)
|
25
|
+
```
|
26
|
+
|
27
|
+
### 2. Encryption Module
|
28
|
+
|
29
|
+
Handles the cryptographic operations:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
module Familia::Encryption
|
33
|
+
# Encrypts with field-specific derived key
|
34
|
+
def self.encrypt(plaintext, context:, additional_data: nil)
|
35
|
+
|
36
|
+
# Decrypts and verifies authenticity
|
37
|
+
def self.decrypt(ciphertext, context:, additional_data: nil)
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
### 3. Key Derivation
|
42
|
+
|
43
|
+
Each field gets a unique encryption key:
|
44
|
+
|
45
|
+
```
|
46
|
+
Master Key + Field Context → HKDF/BLAKE2b → Field-Specific Key
|
47
|
+
```
|
48
|
+
|
49
|
+
## Implementation Steps
|
50
|
+
|
51
|
+
### Step 1: Enable Encryption
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class MyModel < Familia::Horreum
|
55
|
+
# Add the feature (optional if globally enabled)
|
56
|
+
feature :encryption
|
57
|
+
|
58
|
+
# Define encrypted fields
|
59
|
+
encrypted_field :sensitive_data
|
60
|
+
encrypted_field :api_key
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Step 2: Configure Keys
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# config/initializers/familia.rb
|
68
|
+
Familia.configure do |config|
|
69
|
+
config.encryption_keys = {
|
70
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY_V1']
|
71
|
+
}
|
72
|
+
config.current_key_version = :v1
|
73
|
+
end
|
74
|
+
|
75
|
+
# Validate configuration at startup
|
76
|
+
Familia::Encryption.validate_configuration!
|
77
|
+
```
|
78
|
+
|
79
|
+
### Step 3: Generate Keys
|
80
|
+
|
81
|
+
```bash
|
82
|
+
# Generate a secure key
|
83
|
+
$ familia encryption:generate_key --bits 256
|
84
|
+
# => base64_encoded_key_here
|
85
|
+
|
86
|
+
# Add to environment
|
87
|
+
$ echo "FAMILIA_ENCRYPTION_KEY_V1=base64_encoded_key_here" >> .env
|
88
|
+
```
|
89
|
+
|
90
|
+
## Advanced Usage
|
91
|
+
|
92
|
+
### Custom Field Names
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
encrypted_field :favorite_snack, as: :top_secret_snack_preference
|
96
|
+
```
|
97
|
+
|
98
|
+
### Passphrase Protection
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class Vault < Familia::Horreum
|
102
|
+
encrypted_field :secret
|
103
|
+
|
104
|
+
def unlock(passphrase)
|
105
|
+
# Passphrase becomes part of encryption context
|
106
|
+
self.secret(passphrase_value: passphrase)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
### Batch Operations
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# Efficient bulk encryption
|
115
|
+
customers = Customer.batch_create([
|
116
|
+
{ email: 'user1@example.com', favorite_snack: 'chocolate chip cookies' },
|
117
|
+
{ email: 'user2@example.com', favorite_snack: 'leftover pizza' }
|
118
|
+
])
|
119
|
+
```
|
120
|
+
|
121
|
+
## Performance Optimization
|
122
|
+
|
123
|
+
### Request-Scoped Key Caching
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
# Automatically enabled in web frameworks
|
127
|
+
# Manual control for other contexts:
|
128
|
+
Familia::Encryption.with_key_cache do
|
129
|
+
# All operations here share derived keys
|
130
|
+
Customer.find_each { |c| c.process }
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
### Memory Management
|
135
|
+
|
136
|
+
With libsodium installed:
|
137
|
+
- Keys are automatically wiped from memory
|
138
|
+
- Plaintext values cleared after use
|
139
|
+
|
140
|
+
## Testing
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
# Test helper
|
144
|
+
RSpec.configure do |config|
|
145
|
+
config.include Familia::EncryptionTestHelpers
|
146
|
+
|
147
|
+
config.around(:each, :encryption) do |example|
|
148
|
+
with_test_encryption_keys { example.run }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# In tests
|
153
|
+
it "encrypts sensitive fields", :encryption do
|
154
|
+
user = User.create(favorite_snack: "leftover pizza")
|
155
|
+
|
156
|
+
# Verify encryption in Redis
|
157
|
+
raw_value = redis.hget(user.rediskey, "favorite_snack")
|
158
|
+
expect(raw_value).not_to include("leftover pizza")
|
159
|
+
expect(JSON.parse(raw_value)).to have_key("ciphertext")
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
## Troubleshooting
|
164
|
+
|
165
|
+
### Common Issues
|
166
|
+
|
167
|
+
1. **"No encryption key configured"**
|
168
|
+
- Ensure `FAMILIA_ENCRYPTION_KEY` is set
|
169
|
+
- Check `Familia.config.encryption_keys`
|
170
|
+
|
171
|
+
2. **"Decryption failed"**
|
172
|
+
- Verify correct key version
|
173
|
+
- Check if data was encrypted with different key
|
174
|
+
|
175
|
+
3. **Performance degradation**
|
176
|
+
- Enable key caching
|
177
|
+
- Consider installing libsodium gem
|
178
|
+
|
179
|
+
## Next Steps
|
180
|
+
|
181
|
+
- [Security Model](Security-Model) - Understand the cryptographic design
|
182
|
+
- [Key Management](Key-Management) - Rotation and best practices
|
183
|
+
- [Migration Guide](Migration-Guide) - Upgrade existing fields
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# Security Model
|
2
|
+
|
3
|
+
## Cryptographic Design
|
4
|
+
|
5
|
+
### Encryption Algorithms
|
6
|
+
|
7
|
+
**Primary (with libsodium):**
|
8
|
+
- Algorithm: XChaCha20-Poly1305
|
9
|
+
- Key Size: 256 bits
|
10
|
+
- Nonce Size: 192 bits
|
11
|
+
- Authentication Tag: 128 bits
|
12
|
+
|
13
|
+
**Fallback (with OpenSSL):**
|
14
|
+
- Algorithm: AES-256-GCM
|
15
|
+
- Key Size: 256 bits
|
16
|
+
- IV Size: 96 bits
|
17
|
+
- Authentication Tag: 128 bits
|
18
|
+
|
19
|
+
### Key Derivation
|
20
|
+
|
21
|
+
Each field gets a unique key derived from the master key:
|
22
|
+
|
23
|
+
```
|
24
|
+
Field Key = KDF(Master Key, Context)
|
25
|
+
|
26
|
+
Where Context = "ClassName:field_name:record_identifier"
|
27
|
+
```
|
28
|
+
|
29
|
+
**KDF Functions:**
|
30
|
+
- Libsodium: BLAKE2b with personalization
|
31
|
+
- OpenSSL: HKDF-SHA256
|
32
|
+
|
33
|
+
### Ciphertext Format
|
34
|
+
|
35
|
+
```json
|
36
|
+
{
|
37
|
+
"library": "libsodium",
|
38
|
+
"algorithm": "xchacha20poly1305",
|
39
|
+
"nonce": "base64_encoded_nonce",
|
40
|
+
"ciphertext": "base64_encoded_ciphertext",
|
41
|
+
"key_version": "v1_2504"
|
42
|
+
}
|
43
|
+
```
|
44
|
+
|
45
|
+
## Threat Model
|
46
|
+
|
47
|
+
### Protected Against
|
48
|
+
|
49
|
+
#### Database Compromise
|
50
|
+
- All sensitive fields encrypted with strong keys
|
51
|
+
- Attackers see only ciphertext
|
52
|
+
|
53
|
+
#### Field Value Swapping
|
54
|
+
- Field-specific key derivation prevents cross-field decryption
|
55
|
+
- Swapped values fail to decrypt
|
56
|
+
|
57
|
+
#### Replay Attacks
|
58
|
+
- Each encryption uses unique random nonce
|
59
|
+
- Old values remain valid but are distinct encryptions
|
60
|
+
|
61
|
+
#### Tampering
|
62
|
+
- Authenticated encryption (Poly1305/GCM)
|
63
|
+
- Modified ciphertext fails authentication
|
64
|
+
|
65
|
+
### Not Protected Against
|
66
|
+
|
67
|
+
#### Application Memory Compromise
|
68
|
+
- Plaintext values exist in Ruby memory
|
69
|
+
- Mitigation: Use libsodium for memory wiping, minimize plaintext lifetime
|
70
|
+
|
71
|
+
#### Master Key Compromise
|
72
|
+
- All encrypted data compromised if keys obtained
|
73
|
+
- Mitigation: Secure key storage, regular rotation, hardware security modules
|
74
|
+
|
75
|
+
#### Side-Channel Attacks
|
76
|
+
- Key recovery through timing/power analysis
|
77
|
+
- Mitigation: Libsodium provides constant-time operations
|
78
|
+
|
79
|
+
## Additional Security Features
|
80
|
+
|
81
|
+
### Passphrase Protection
|
82
|
+
|
83
|
+
For ultra-sensitive fields, add user passphrases:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
encrypted_field :love_letter
|
87
|
+
|
88
|
+
# Passphrase required for decryption
|
89
|
+
vault.love_letter(passphrase_value: user_passphrase)
|
90
|
+
```
|
91
|
+
|
92
|
+
**How it works:**
|
93
|
+
1. Passphrase hashed with SHA-256
|
94
|
+
2. Hash included in Additional Authenticated Data (AAD)
|
95
|
+
3. Wrong passphrase = authentication failure
|
96
|
+
4. Passphrase never stored, only verified
|
97
|
+
|
98
|
+
### Memory Safety
|
99
|
+
|
100
|
+
**With libsodium:**
|
101
|
+
- Automatic zeroing of sensitive memory
|
102
|
+
- Constant-time comparisons
|
103
|
+
- Protected memory pages when available
|
104
|
+
|
105
|
+
**Without libsodium:**
|
106
|
+
- Warning logged about reduced security
|
107
|
+
- Ruby GC may retain plaintext copies
|
108
|
+
- Timing attacks theoretically possible
|
109
|
+
|
110
|
+
### RedactedString
|
111
|
+
|
112
|
+
Prevents accidental logging of sensitive data:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class RedactedString < String
|
116
|
+
def to_s
|
117
|
+
'[REDACTED]'
|
118
|
+
end
|
119
|
+
|
120
|
+
def inspect
|
121
|
+
'[REDACTED]'
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# In logs:
|
126
|
+
logger.info "Love letter: #{user.love_letter}" # => "Love letter: [REDACTED]"
|
127
|
+
```
|
128
|
+
|
129
|
+
## Security Checklist
|
130
|
+
|
131
|
+
### Development
|
132
|
+
|
133
|
+
- [ ] Never log plaintext sensitive fields
|
134
|
+
- [ ] Use RedactedString for extra protection
|
135
|
+
- [ ] Use libsodium for production when possible
|
136
|
+
- [ ] Validate encryption at startup
|
137
|
+
- [ ] Test encryption round-trips
|
138
|
+
|
139
|
+
### Operations
|
140
|
+
|
141
|
+
- [ ] Regular key rotation schedule
|
142
|
+
- [ ] Monitor decryption failures
|
143
|
+
- [ ] Log field access patterns for auditing purposes
|
data/lib/familia/base.rb
CHANGED
@@ -14,7 +14,8 @@ module Familia
|
|
14
14
|
# @see Familia::DataType
|
15
15
|
#
|
16
16
|
module Base
|
17
|
-
@
|
17
|
+
@features_available = nil
|
18
|
+
@feature_definitions = nil
|
18
19
|
@dump_method = :to_json
|
19
20
|
@load_method = :from_json
|
20
21
|
|
@@ -29,43 +30,33 @@ module Familia
|
|
29
30
|
end
|
30
31
|
|
31
32
|
class << self
|
32
|
-
attr_reader :
|
33
|
+
attr_reader :features_available, :feature_definitions
|
33
34
|
attr_accessor :dump_method, :load_method
|
34
35
|
|
35
|
-
def add_feature(klass,
|
36
|
-
@
|
37
|
-
Familia.ld "[#{self}] Adding feature #{klass} as #{
|
36
|
+
def add_feature(klass, feature_name, depends_on: [])
|
37
|
+
@features_available ||= {}
|
38
|
+
Familia.ld "[#{self}] Adding feature #{klass} as #{feature_name.inspect}"
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
# Create field definition object
|
41
|
+
feature_def = FeatureDefinition.new(
|
42
|
+
name: feature_name,
|
43
|
+
depends_on: depends_on,
|
44
|
+
)
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
#
|
50
|
-
# @param default_expiration [Integer, nil] Time To Live in seconds (ignored in base implementation)
|
51
|
-
# @return [nil] Always returns nil
|
52
|
-
#
|
53
|
-
# @note This is a no-op implementation. Classes that need expiration
|
54
|
-
# functionality should include the :expiration feature.
|
55
|
-
#
|
56
|
-
def update_expiration(default_expiration: nil)
|
57
|
-
Familia.ld "[update_expiration] Feature not enabled for #{self.class}. Key: #{dbkey} (caller: #{caller(1..1)})"
|
58
|
-
nil
|
46
|
+
# Track field definitions after defining field methods
|
47
|
+
@feature_definitions ||= {}
|
48
|
+
@feature_definitions[feature_name] = feature_def
|
49
|
+
|
50
|
+
features_available[feature_name] = klass
|
51
|
+
end
|
59
52
|
end
|
60
53
|
|
61
54
|
def generate_id
|
62
|
-
@identifier ||= Familia.generate_id
|
63
|
-
@identifier
|
55
|
+
@identifier ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
|
64
56
|
end
|
65
57
|
|
66
58
|
def uuid
|
67
59
|
@uuid ||= SecureRandom.uuid
|
68
|
-
@uuid
|
69
60
|
end
|
70
61
|
end
|
71
62
|
end
|
data/lib/familia/connection.rb
CHANGED
@@ -106,18 +106,19 @@ module Familia
|
|
106
106
|
# Always pass normalized URI with database to provider
|
107
107
|
# Provider MUST return connection already on the correct database
|
108
108
|
parsed_uri = normalize_uri(uri)
|
109
|
-
|
109
|
+
client = connection_provider.call(parsed_uri.to_s)
|
110
110
|
|
111
111
|
# In debug mode, verify the provider honored the contract
|
112
|
-
if Familia.debug? &&
|
113
|
-
current_db =
|
112
|
+
if Familia.debug? && client.respond_to?(:client)
|
113
|
+
current_db = client.connection[:db]
|
114
114
|
expected_db = parsed_uri.db || 0
|
115
|
+
Familia.ld "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
|
115
116
|
if current_db != expected_db
|
116
|
-
Familia.warn "Connection provider returned
|
117
|
+
Familia.warn "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
|
117
118
|
end
|
118
119
|
end
|
119
120
|
|
120
|
-
return
|
121
|
+
return client
|
121
122
|
end
|
122
123
|
|
123
124
|
# Third priority: Fallback behavior or error
|
@@ -1,11 +1,9 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/commands.rb
|
2
2
|
|
3
3
|
class Familia::DataType
|
4
|
-
|
5
4
|
# Must be included in all DataType classes to provide Redis
|
6
5
|
# commands. The class must have a dbkey method.
|
7
6
|
module Commands
|
8
|
-
|
9
7
|
def move(logical_database)
|
10
8
|
dbclient.move dbkey, logical_database
|
11
9
|
end
|
@@ -52,8 +50,7 @@ class Familia::DataType
|
|
52
50
|
end
|
53
51
|
|
54
52
|
def echo(meth, trace)
|
55
|
-
dbclient.echo "[#{self.class}
|
53
|
+
dbclient.echo "[#{self.class}##{meth}] #{trace} (#{@opts[:class]}#)"
|
56
54
|
end
|
57
|
-
|
58
55
|
end
|
59
56
|
end
|
@@ -1,9 +1,7 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/serialization.rb
|
2
2
|
|
3
3
|
class Familia::DataType
|
4
|
-
|
5
4
|
module Serialization
|
6
|
-
|
7
5
|
# Serializes a value for storage in Redis.
|
8
6
|
#
|
9
7
|
# @param val [Object] The value to be serialized.
|
@@ -33,7 +31,7 @@ class Familia::DataType
|
|
33
31
|
|
34
32
|
if opts[:class]
|
35
33
|
prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
|
36
|
-
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared||'<nil>'}"
|
34
|
+
Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared || '<nil>'}"
|
37
35
|
end
|
38
36
|
|
39
37
|
if prepared.nil?
|
@@ -42,9 +40,12 @@ class Familia::DataType
|
|
42
40
|
Familia.ld " from <#{val.class}> => <#{prepared.class}>"
|
43
41
|
end
|
44
42
|
|
45
|
-
|
43
|
+
if Familia.debug?
|
44
|
+
Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>",
|
45
|
+
caller(1..1)
|
46
|
+
end
|
46
47
|
|
47
|
-
Familia.warn "[#{self.class}
|
48
|
+
Familia.warn "[#{self.class}#serialize_value] nil returned for #{opts[:class]}##{name}" if prepared.nil?
|
48
49
|
prepared
|
49
50
|
end
|
50
51
|
|
@@ -88,9 +89,7 @@ class Familia::DataType
|
|
88
89
|
next if obj.nil?
|
89
90
|
|
90
91
|
val = @opts[:class].send load_method, obj
|
91
|
-
if val.nil?
|
92
|
-
Familia.ld "[#{self.class}\#deserialize_values] nil returned for #{@opts[:class]}\##{name}"
|
93
|
-
end
|
92
|
+
Familia.ld "[#{self.class}#deserialize_values] nil returned for #{@opts[:class]}##{name}" if val.nil?
|
94
93
|
|
95
94
|
val
|
96
95
|
rescue StandardError => e
|
@@ -125,5 +124,4 @@ class Familia::DataType
|
|
125
124
|
ret&.first # return the object or nil
|
126
125
|
end
|
127
126
|
end
|
128
|
-
|
129
127
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/types/hashkey.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class HashKey < DataType
|
@@ -55,7 +55,7 @@ module Familia
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def hgetall
|
58
|
-
dbclient.hgetall(dbkey).each_with_object({}) do |(k,v), ret|
|
58
|
+
dbclient.hgetall(dbkey).each_with_object({}) do |(k, v), ret|
|
59
59
|
ret[k] = deserialize_value v
|
60
60
|
end
|
61
61
|
end
|
@@ -1,8 +1,7 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/types/list.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class List < DataType
|
5
|
-
|
6
5
|
# Returns the number of elements in the list
|
7
6
|
# @return [Integer] number of elements
|
8
7
|
def element_count
|
@@ -91,36 +90,36 @@ module Familia
|
|
91
90
|
rangeraw 0, count
|
92
91
|
end
|
93
92
|
|
94
|
-
def each(&
|
95
|
-
range.each(&
|
93
|
+
def each(&)
|
94
|
+
range.each(&)
|
96
95
|
end
|
97
96
|
|
98
|
-
def each_with_index(&
|
99
|
-
range.each_with_index(&
|
97
|
+
def each_with_index(&)
|
98
|
+
range.each_with_index(&)
|
100
99
|
end
|
101
100
|
|
102
|
-
def eachraw(&
|
103
|
-
rangeraw.each(&
|
101
|
+
def eachraw(&)
|
102
|
+
rangeraw.each(&)
|
104
103
|
end
|
105
104
|
|
106
|
-
def eachraw_with_index(&
|
107
|
-
rangeraw.each_with_index(&
|
105
|
+
def eachraw_with_index(&)
|
106
|
+
rangeraw.each_with_index(&)
|
108
107
|
end
|
109
108
|
|
110
|
-
def collect(&
|
111
|
-
range.collect(&
|
109
|
+
def collect(&)
|
110
|
+
range.collect(&)
|
112
111
|
end
|
113
112
|
|
114
|
-
def select(&
|
115
|
-
range.select(&
|
113
|
+
def select(&)
|
114
|
+
range.select(&)
|
116
115
|
end
|
117
116
|
|
118
|
-
def collectraw(&
|
119
|
-
rangeraw.collect(&
|
117
|
+
def collectraw(&)
|
118
|
+
rangeraw.collect(&)
|
120
119
|
end
|
121
120
|
|
122
|
-
def selectraw(&
|
123
|
-
rangeraw.select(&
|
121
|
+
def selectraw(&)
|
122
|
+
rangeraw.select(&)
|
124
123
|
end
|
125
124
|
|
126
125
|
def at(idx)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# lib/familia/
|
1
|
+
# lib/familia/data_type/types/sorted_set.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
class SortedSet < DataType
|
@@ -99,36 +99,36 @@ module Familia
|
|
99
99
|
revrangeraw 0, count, opts
|
100
100
|
end
|
101
101
|
|
102
|
-
def each(&
|
103
|
-
members.each(&
|
102
|
+
def each(&)
|
103
|
+
members.each(&)
|
104
104
|
end
|
105
105
|
|
106
|
-
def each_with_index(&
|
107
|
-
members.each_with_index(&
|
106
|
+
def each_with_index(&)
|
107
|
+
members.each_with_index(&)
|
108
108
|
end
|
109
109
|
|
110
|
-
def collect(&
|
111
|
-
members.collect(&
|
110
|
+
def collect(&)
|
111
|
+
members.collect(&)
|
112
112
|
end
|
113
113
|
|
114
|
-
def select(&
|
115
|
-
members.select(&
|
114
|
+
def select(&)
|
115
|
+
members.select(&)
|
116
116
|
end
|
117
117
|
|
118
|
-
def eachraw(&
|
119
|
-
membersraw.each(&
|
118
|
+
def eachraw(&)
|
119
|
+
membersraw.each(&)
|
120
120
|
end
|
121
121
|
|
122
|
-
def eachraw_with_index(&
|
123
|
-
membersraw.each_with_index(&
|
122
|
+
def eachraw_with_index(&)
|
123
|
+
membersraw.each_with_index(&)
|
124
124
|
end
|
125
125
|
|
126
|
-
def collectraw(&
|
127
|
-
membersraw.collect(&
|
126
|
+
def collectraw(&)
|
127
|
+
membersraw.collect(&)
|
128
128
|
end
|
129
129
|
|
130
|
-
def selectraw(&
|
131
|
-
membersraw.select(&
|
130
|
+
def selectraw(&)
|
131
|
+
membersraw.select(&)
|
132
132
|
end
|
133
133
|
|
134
134
|
def range(sidx, eidx, opts = {})
|