familia 2.0.0.pre7 → 2.0.0.pre10
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 +13 -0
- data/.github/workflows/docs.yml +1 -1
- data/.gitignore +9 -9
- data/.rubocop.yml +19 -0
- data/.yardopts +22 -1
- data/CHANGELOG.md +184 -0
- data/CLAUDE.md +8 -5
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +97 -2
- data/changelog.d/README.md +66 -0
- data/changelog.d/fragments/.keep +0 -0
- data/changelog.d/template.md.j2 +29 -0
- data/docs/archive/.gitignore +2 -0
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +210 -0
- data/docs/archive/FAMILIA_TECHNICAL.md +823 -0
- data/docs/archive/FAMILIA_UPDATE.md +226 -0
- data/docs/archive/README.md +67 -0
- data/docs/guides/.gitignore +2 -0
- data/docs/{wiki → guides}/Feature-System-Guide.md +0 -15
- data/docs/{wiki → guides}/Relationships-Guide.md +103 -50
- data/docs/guides/relationships-methods.md +266 -0
- data/examples/relationships_basic.rb +90 -157
- data/familia.gemspec +4 -4
- data/lib/familia/connection.rb +4 -21
- data/lib/familia/features/external_identifiers/external_identifier_field_type.rb +120 -0
- data/lib/familia/features/external_identifiers.rb +111 -0
- data/lib/familia/features/object_identifiers/object_identifier_field_type.rb +91 -0
- data/lib/familia/features/object_identifiers.rb +194 -0
- data/lib/familia/features/relationships/cascading.rb +0 -1
- data/lib/familia/features/relationships/indexing.rb +160 -176
- data/lib/familia/features/relationships/membership.rb +16 -22
- data/lib/familia/features/relationships/querying.rb +7 -12
- data/lib/familia/features/relationships/score_encoding.rb +1 -3
- data/lib/familia/features/relationships/tracking.rb +61 -22
- data/lib/familia/features/relationships.rb +15 -8
- data/lib/familia/features/transient_fields.rb +8 -10
- data/lib/familia/features.rb +16 -13
- data/lib/familia/horreum/core/serialization.rb +2 -5
- data/lib/familia/horreum/subclass/definition.rb +36 -0
- data/lib/familia/horreum.rb +15 -24
- data/lib/familia/version.rb +1 -3
- data/setup.cfg +12 -0
- data/try/core/errors_try.rb +1 -1
- data/try/features/{encrypted_fields_core_try.rb → encrypted_fields/encrypted_fields_core_try.rb} +1 -1
- data/try/features/{encrypted_fields_integration_try.rb → encrypted_fields/encrypted_fields_integration_try.rb} +1 -1
- data/try/features/{encrypted_fields_no_cache_security_try.rb → encrypted_fields/encrypted_fields_no_cache_security_try.rb} +1 -1
- data/try/features/{encrypted_fields_security_try.rb → encrypted_fields/encrypted_fields_security_try.rb} +1 -1
- data/try/features/{expiration_try.rb → expiration/expiration_try.rb} +1 -1
- data/try/features/external_identifiers/external_identifiers_try.rb +203 -0
- data/try/features/object_identifiers/object_identifiers_integration_try.rb +289 -0
- data/try/features/object_identifiers/object_identifiers_try.rb +191 -0
- data/try/features/{quantization_try.rb → quantization/quantization_try.rb} +1 -1
- data/try/features/{categorical_permissions_try.rb → relationships/categorical_permissions_try.rb} +1 -1
- data/try/features/relationships/relationships_api_changes_try.rb +339 -0
- data/try/features/{relationships_edge_cases_try.rb → relationships/relationships_edge_cases_try.rb} +1 -1
- data/try/features/{relationships_performance_minimal_try.rb → relationships/relationships_performance_minimal_try.rb} +1 -1
- data/try/features/{relationships_performance_simple_try.rb → relationships/relationships_performance_simple_try.rb} +1 -1
- data/try/features/{relationships_performance_try.rb → relationships/relationships_performance_try.rb} +1 -1
- data/try/features/{relationships_performance_working_try.rb → relationships/relationships_performance_working_try.rb} +1 -1
- data/try/features/{relationships_try.rb → relationships/relationships_try.rb} +7 -6
- data/try/features/{safe_dump_advanced_try.rb → safe_dump/safe_dump_advanced_try.rb} +1 -1
- data/try/features/{safe_dump_try.rb → safe_dump/safe_dump_try.rb} +1 -1
- data/try/features/{transient_fields_core_try.rb → transient_fields/transient_fields_core_try.rb} +1 -1
- data/try/features/{transient_fields_integration_try.rb → transient_fields/transient_fields_integration_try.rb} +1 -1
- metadata +80 -60
- /data/docs/{wiki → guides}/API-Reference.md +0 -0
- /data/docs/{wiki → guides}/Connection-Pooling-Guide.md +0 -0
- /data/docs/{wiki → guides}/Encrypted-Fields-Overview.md +0 -0
- /data/docs/{wiki → guides}/Expiration-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Features-System-Developer-Guide.md +0 -0
- /data/docs/{wiki → guides}/Field-System-Guide.md +0 -0
- /data/docs/{wiki → guides}/Home.md +0 -0
- /data/docs/{wiki → guides}/Implementation-Guide.md +0 -0
- /data/docs/{wiki → guides}/Quantization-Feature-Guide.md +0 -0
- /data/docs/{wiki → guides}/Security-Model.md +0 -0
- /data/docs/{wiki → guides}/Transient-Fields-Guide.md +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/aad_protection_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/concealed_string_core_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/context_isolation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/error_conditions_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_derivation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/fresh_key_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/key_rotation_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/memory_security_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/missing_current_key_version_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/nonce_uniqueness_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/secure_by_default_behavior_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/thread_safety_try.rb +0 -0
- /data/try/features/{encryption_fields → encrypted_fields}/universal_serialization_safety_try.rb +0 -0
@@ -0,0 +1,823 @@
|
|
1
|
+
# Familia v2.0.0-pre Series Technical Reference
|
2
|
+
|
3
|
+
**Familia** is a Ruby ORM for Redis/Valkey providing object mapping, relationships, and advanced features like encryption, connection pooling, and permission systems. This technical reference covers the major classes, methods, and usage patterns introduced in the v2.0.0-pre series.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## Core Architecture
|
8
|
+
|
9
|
+
### Base Classes
|
10
|
+
|
11
|
+
#### `Familia::Horreum` - Primary ORM Base Class
|
12
|
+
The main base class for Redis-backed objects, similar to ActiveRecord models.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
class User < Familia::Horreum
|
16
|
+
# Basic field definitions
|
17
|
+
field :name, :email, :created_at
|
18
|
+
|
19
|
+
# Redis data types as instance variables
|
20
|
+
list :sessions # Redis list
|
21
|
+
set :tags # Redis set
|
22
|
+
sorted_set :scores # Redis sorted set
|
23
|
+
hash :settings # Redis hash
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
**Key Methods:**
|
28
|
+
- `save` - Persist object to Redis
|
29
|
+
- `save_if_not_exists` - Conditional persistence (v2.0.0-pre6)
|
30
|
+
- `load` - Load object from Redis
|
31
|
+
- `exists?` - Check if object exists in Redis
|
32
|
+
- `destroy` - Remove object from Redis
|
33
|
+
|
34
|
+
#### `Familia::DataType` - Redis Data Type Wrapper
|
35
|
+
Base class for Redis data type implementations.
|
36
|
+
|
37
|
+
**Registered Types:**
|
38
|
+
- `String` - Redis strings
|
39
|
+
- `List` - Redis lists
|
40
|
+
- `Set` - Redis sets
|
41
|
+
- `SortedSet` - Redis sorted sets
|
42
|
+
- `HashKey` - Redis hashes
|
43
|
+
- `Counter` - Atomic counters
|
44
|
+
- `Lock` - Distributed locks
|
45
|
+
|
46
|
+
---
|
47
|
+
|
48
|
+
## Feature System (v2.0.0-pre5+)
|
49
|
+
|
50
|
+
### Feature Architecture
|
51
|
+
Modular system for extending Horreum classes with reusable functionality.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class Customer < Familia::Horreum
|
55
|
+
feature :expiration # TTL management
|
56
|
+
feature :safe_dump # API-safe serialization
|
57
|
+
feature :encrypted_fields # Field encryption
|
58
|
+
feature :transient_fields # Non-persistent fields
|
59
|
+
feature :relationships # Object relationships
|
60
|
+
|
61
|
+
field :name, :email
|
62
|
+
encrypted_field :api_key
|
63
|
+
transient_field :password
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
### Built-in Features
|
68
|
+
|
69
|
+
#### 1. Expiration Feature
|
70
|
+
TTL (Time To Live) management with cascading expiration.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class Session < Familia::Horreum
|
74
|
+
feature :expiration
|
75
|
+
|
76
|
+
field :user_id, :token
|
77
|
+
default_expiration 24.hours
|
78
|
+
|
79
|
+
# Cascade expiration to related objects
|
80
|
+
cascade_expiration_to :user_activity
|
81
|
+
end
|
82
|
+
|
83
|
+
session = Session.new(user_id: 123, token: "abc123")
|
84
|
+
session.save
|
85
|
+
session.expire_in(1.hour) # Set custom expiration
|
86
|
+
session.ttl # Check remaining time
|
87
|
+
```
|
88
|
+
|
89
|
+
#### 2. SafeDump Feature
|
90
|
+
API-safe serialization excluding sensitive fields.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class User < Familia::Horreum
|
94
|
+
feature :safe_dump
|
95
|
+
|
96
|
+
field :name, :email, :password_hash
|
97
|
+
safe_dump_fields :name, :email # Only these fields in safe_dump
|
98
|
+
end
|
99
|
+
|
100
|
+
user = User.new(name: "Alice", email: "alice@example.com", password_hash: "secret")
|
101
|
+
user.safe_dump # => {"name" => "Alice", "email" => "alice@example.com"}
|
102
|
+
```
|
103
|
+
|
104
|
+
#### 3. Encrypted Fields Feature (v2.0.0-pre5)
|
105
|
+
Transparent field-level encryption with multiple providers.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# Configuration
|
109
|
+
Familia.configure do |config|
|
110
|
+
config.encryption_keys = {
|
111
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY'],
|
112
|
+
v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
|
113
|
+
}
|
114
|
+
config.current_key_version = :v2
|
115
|
+
end
|
116
|
+
|
117
|
+
class Vault < Familia::Horreum
|
118
|
+
feature :encrypted_fields
|
119
|
+
|
120
|
+
field :name # Plaintext
|
121
|
+
encrypted_field :secret_key # Encrypted with XChaCha20-Poly1305
|
122
|
+
encrypted_field :api_token # Field-specific key derivation
|
123
|
+
encrypted_field :private_data # Transparent access
|
124
|
+
end
|
125
|
+
|
126
|
+
vault = Vault.new(
|
127
|
+
name: "Production Secrets",
|
128
|
+
secret_key: "super-secret-123",
|
129
|
+
api_token: "sk-1234567890"
|
130
|
+
)
|
131
|
+
vault.save
|
132
|
+
|
133
|
+
# Transparent access - automatically encrypted/decrypted
|
134
|
+
vault.secret_key # => "super-secret-123" (decrypted on access)
|
135
|
+
```
|
136
|
+
|
137
|
+
**Encryption Providers:**
|
138
|
+
- **XChaCha20-Poly1305** (preferred) - Requires `rbnacl` gem
|
139
|
+
- **AES-256-GCM** (fallback) - Uses OpenSSL, no dependencies
|
140
|
+
|
141
|
+
**Security Features:**
|
142
|
+
- Field-specific key derivation for domain separation
|
143
|
+
- Key versioning and rotation support
|
144
|
+
- Memory-safe key handling
|
145
|
+
- Configurable encryption algorithms
|
146
|
+
|
147
|
+
#### 4. Transient Fields Feature (v2.0.0-pre5)
|
148
|
+
Non-persistent fields with memory-safe handling.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
class LoginForm < Familia::Horreum
|
152
|
+
feature :transient_fields
|
153
|
+
|
154
|
+
field :username # Persistent
|
155
|
+
transient_field :password # Never stored in Redis
|
156
|
+
transient_field :csrf_token # Runtime only
|
157
|
+
end
|
158
|
+
|
159
|
+
form = LoginForm.new(username: "alice", password: "secret123")
|
160
|
+
form.save # Only saves 'username', password is transient
|
161
|
+
|
162
|
+
form.password.class # => Familia::Features::TransientFields::RedactedString
|
163
|
+
form.password.to_s # => "[REDACTED]" (safe for logging)
|
164
|
+
form.password.reveal # => "secret123" (explicit access)
|
165
|
+
```
|
166
|
+
|
167
|
+
**RedactedString** - Security wrapper preventing accidental exposure:
|
168
|
+
- `to_s` returns "[REDACTED]"
|
169
|
+
- `inspect` returns "[REDACTED]"
|
170
|
+
- `reveal` method for explicit access
|
171
|
+
- Safe for logging and serialization
|
172
|
+
|
173
|
+
#### 5. Relationships Feature (v2.0.0-pre7)
|
174
|
+
Comprehensive object relationship system with three relationship types.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
class Customer < Familia::Horreum
|
178
|
+
feature :relationships
|
179
|
+
|
180
|
+
identifier_field :custid
|
181
|
+
field :custid, :name, :email
|
182
|
+
|
183
|
+
# Collections for storing related object IDs
|
184
|
+
set :domains # Simple set
|
185
|
+
sorted_set :activity # Scored/sorted collection
|
186
|
+
|
187
|
+
# Indexed lookups (O(1) hash-based)
|
188
|
+
indexed_by :email_lookup, field: :email
|
189
|
+
|
190
|
+
# Global tracking with scoring
|
191
|
+
tracked_in :all_customers, type: :sorted_set, score: :created_at
|
192
|
+
end
|
193
|
+
|
194
|
+
class Domain < Familia::Horreum
|
195
|
+
feature :relationships
|
196
|
+
|
197
|
+
identifier_field :domain_id
|
198
|
+
field :domain_id, :name, :status
|
199
|
+
|
200
|
+
# Bidirectional membership
|
201
|
+
member_of Customer, :domains, type: :set
|
202
|
+
|
203
|
+
# Conditional tracking with lambda scoring
|
204
|
+
tracked_in :active_domains, type: :sorted_set,
|
205
|
+
score: ->(domain) { domain.status == 'active' ? Time.now.to_i : 0 }
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
**Relationship Operations:**
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
# Create objects
|
213
|
+
customer = Customer.new(custid: "cust123", name: "Acme Corp")
|
214
|
+
domain = Domain.new(domain_id: "dom456", name: "acme.com", status: "active")
|
215
|
+
|
216
|
+
# Establish bidirectional relationship
|
217
|
+
domain.add_to_customer_domains(customer.custid)
|
218
|
+
customer.domains.add(domain.identifier)
|
219
|
+
|
220
|
+
# Query relationships
|
221
|
+
domain.in_customer_domains?(customer.custid) # => true
|
222
|
+
customer.domains.member?(domain.identifier) # => true
|
223
|
+
|
224
|
+
# Indexed lookups
|
225
|
+
Customer.add_to_email_lookup(customer)
|
226
|
+
found_id = Customer.email_lookup.get(customer.email) # O(1) lookup
|
227
|
+
|
228
|
+
# Global tracking
|
229
|
+
Customer.add_to_all_customers(customer)
|
230
|
+
recent = Customer.all_customers.range_by_score(
|
231
|
+
(Time.now - 24.hours).to_i, '+inf'
|
232
|
+
)
|
233
|
+
```
|
234
|
+
|
235
|
+
---
|
236
|
+
|
237
|
+
## Connection Management (v2.0.0-pre+)
|
238
|
+
|
239
|
+
### Connection Provider Pattern
|
240
|
+
Flexible connection pooling with provider-based architecture.
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
# Basic Redis connection
|
244
|
+
Familia.configure do |config|
|
245
|
+
config.redis_uri = "redis://localhost:6379/0"
|
246
|
+
end
|
247
|
+
|
248
|
+
# Connection pooling with ConnectionPool gem
|
249
|
+
require 'connection_pool'
|
250
|
+
|
251
|
+
Familia.connection_provider = lambda do |uri|
|
252
|
+
parsed = URI.parse(uri)
|
253
|
+
pool_key = "#{parsed.host}:#{parsed.port}/#{parsed.db || 0}"
|
254
|
+
|
255
|
+
@pools ||= {}
|
256
|
+
@pools[pool_key] ||= ConnectionPool.new(size: 10, timeout: 5) do
|
257
|
+
Redis.new(
|
258
|
+
host: parsed.host,
|
259
|
+
port: parsed.port,
|
260
|
+
db: parsed.db || 0,
|
261
|
+
connect_timeout: 1,
|
262
|
+
read_timeout: 1,
|
263
|
+
write_timeout: 1
|
264
|
+
)
|
265
|
+
end
|
266
|
+
|
267
|
+
@pools[pool_key].with { |conn| yield conn if block_given?; conn }
|
268
|
+
end
|
269
|
+
```
|
270
|
+
|
271
|
+
### Multi-Database Support
|
272
|
+
Configure different logical databases for different models.
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
class User < Familia::Horreum
|
276
|
+
logical_database 0 # Use database 0
|
277
|
+
field :name, :email
|
278
|
+
end
|
279
|
+
|
280
|
+
class Session < Familia::Horreum
|
281
|
+
logical_database 1 # Use database 1
|
282
|
+
field :user_id, :token
|
283
|
+
end
|
284
|
+
|
285
|
+
class Cache < Familia::Horreum
|
286
|
+
logical_database 2 # Use database 2
|
287
|
+
field :key, :value
|
288
|
+
end
|
289
|
+
```
|
290
|
+
|
291
|
+
---
|
292
|
+
|
293
|
+
## Advanced Relationship Patterns
|
294
|
+
|
295
|
+
### Permission-Encoded Relationships (v2.0.0-pre7)
|
296
|
+
Combine timestamps with permission bits for access control.
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
class Document < Familia::Horreum
|
300
|
+
feature :relationships
|
301
|
+
|
302
|
+
identifier_field :doc_id
|
303
|
+
field :doc_id, :title, :content
|
304
|
+
|
305
|
+
# Permission constants (bit flags)
|
306
|
+
READ = 1 # 001
|
307
|
+
WRITE = 2 # 010
|
308
|
+
DELETE = 4 # 100
|
309
|
+
ADMIN = 8 # 1000
|
310
|
+
end
|
311
|
+
|
312
|
+
class UserDocumentAccess
|
313
|
+
# Encode timestamp + permissions into sorted set score
|
314
|
+
def self.encode_score(timestamp, permissions)
|
315
|
+
"#{timestamp}.#{permissions}".to_f
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.decode_score(score)
|
319
|
+
parts = score.to_s.split('.')
|
320
|
+
timestamp = parts[0].to_i
|
321
|
+
permissions = parts[1] ? parts[1].to_i : 0
|
322
|
+
[timestamp, permissions]
|
323
|
+
end
|
324
|
+
|
325
|
+
# Check if user has specific permission
|
326
|
+
def self.has_permission?(permissions, required)
|
327
|
+
(permissions & required) != 0
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Usage example
|
332
|
+
user_id = "user123"
|
333
|
+
doc_id = "doc456"
|
334
|
+
timestamp = Time.now.to_i
|
335
|
+
|
336
|
+
# Grant read + write permissions
|
337
|
+
permissions = Document::READ | Document::WRITE # 3
|
338
|
+
score = UserDocumentAccess.encode_score(timestamp, permissions)
|
339
|
+
|
340
|
+
# Store in sorted set (user_id -> score with permissions)
|
341
|
+
user_documents = Familia::DataType::SortedSet.new("user:#{user_id}:documents")
|
342
|
+
user_documents.add(doc_id, score)
|
343
|
+
|
344
|
+
# Query with permission filtering
|
345
|
+
docs_with_write = user_documents.select do |doc_id, score|
|
346
|
+
_, permissions = UserDocumentAccess.decode_score(score)
|
347
|
+
UserDocumentAccess.has_permission?(permissions, Document::WRITE)
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
### Time-Series Relationships
|
352
|
+
Track relationships over time with timestamp-based scoring.
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
class ActivityTracker < Familia::Horreum
|
356
|
+
feature :relationships
|
357
|
+
|
358
|
+
# Track user activities with timestamps
|
359
|
+
tracked_in :user_activities, type: :sorted_set,
|
360
|
+
score: ->(activity) { activity.created_at }
|
361
|
+
|
362
|
+
# Track by activity type
|
363
|
+
tracked_in :activity_by_type, type: :sorted_set,
|
364
|
+
score: ->(activity) { "#{activity.activity_type}:#{activity.created_at}".hash }
|
365
|
+
|
366
|
+
field :user_id, :activity_type, :data, :created_at
|
367
|
+
end
|
368
|
+
|
369
|
+
# Query recent activities (last hour)
|
370
|
+
hour_ago = (Time.now - 1.hour).to_i
|
371
|
+
recent_activities = ActivityTracker.user_activities.range_by_score(
|
372
|
+
hour_ago, '+inf', limit: [0, 50]
|
373
|
+
)
|
374
|
+
|
375
|
+
# Get activities by type in time range
|
376
|
+
login_activities = ActivityTracker.activity_by_type.range_by_score(
|
377
|
+
"login:#{hour_ago}".hash, "login:#{Time.now.to_i}".hash
|
378
|
+
)
|
379
|
+
```
|
380
|
+
|
381
|
+
---
|
382
|
+
|
383
|
+
## Data Type Usage Patterns
|
384
|
+
|
385
|
+
### Advanced Sorted Set Operations
|
386
|
+
Leverage Redis sorted sets for rankings, time series, and scored data.
|
387
|
+
|
388
|
+
```ruby
|
389
|
+
class Leaderboard < Familia::Horreum
|
390
|
+
identifier_field :game_id
|
391
|
+
field :game_id, :name
|
392
|
+
sorted_set :scores
|
393
|
+
end
|
394
|
+
|
395
|
+
leaderboard = Leaderboard.new(game_id: "game1", name: "Daily Challenge")
|
396
|
+
|
397
|
+
# Add player scores
|
398
|
+
leaderboard.scores.add("player1", 1500)
|
399
|
+
leaderboard.scores.add("player2", 2300)
|
400
|
+
leaderboard.scores.add("player3", 1800)
|
401
|
+
|
402
|
+
# Get top 10 players
|
403
|
+
top_players = leaderboard.scores.range(0, 9, with_scores: true, order: 'DESC')
|
404
|
+
# => [["player2", 2300.0], ["player3", 1800.0], ["player1", 1500.0]]
|
405
|
+
|
406
|
+
# Get player rank
|
407
|
+
rank = leaderboard.scores.rank("player1", order: 'DESC') # => 2 (0-indexed)
|
408
|
+
|
409
|
+
# Get score range
|
410
|
+
mid_tier = leaderboard.scores.range_by_score(1000, 2000, with_scores: true)
|
411
|
+
|
412
|
+
# Increment score atomically
|
413
|
+
leaderboard.scores.increment("player1", 100) # Add 100 to existing score
|
414
|
+
```
|
415
|
+
|
416
|
+
### List-Based Queues and Feeds
|
417
|
+
Use Redis lists for queues, feeds, and ordered data.
|
418
|
+
|
419
|
+
```ruby
|
420
|
+
class TaskQueue < Familia::Horreum
|
421
|
+
identifier_field :queue_name
|
422
|
+
field :queue_name
|
423
|
+
list :tasks
|
424
|
+
end
|
425
|
+
|
426
|
+
class ActivityFeed < Familia::Horreum
|
427
|
+
identifier_field :user_id
|
428
|
+
field :user_id
|
429
|
+
list :activities
|
430
|
+
end
|
431
|
+
|
432
|
+
# Task queue operations
|
433
|
+
queue = TaskQueue.new(queue_name: "email_processing")
|
434
|
+
|
435
|
+
# Add tasks to queue (right push)
|
436
|
+
queue.tasks.push({
|
437
|
+
type: "send_email",
|
438
|
+
recipient: "user@example.com",
|
439
|
+
template: "welcome"
|
440
|
+
}.to_json)
|
441
|
+
|
442
|
+
# Process tasks (left pop - FIFO)
|
443
|
+
next_task = queue.tasks.pop # Atomic pop from left
|
444
|
+
task_data = JSON.parse(next_task) if next_task
|
445
|
+
|
446
|
+
# Activity feed with size limit
|
447
|
+
feed = ActivityFeed.new(user_id: "user123")
|
448
|
+
|
449
|
+
# Add activity (keep last 100)
|
450
|
+
feed.activities.unshift("User logged in at #{Time.now}")
|
451
|
+
feed.activities.trim(0, 99) # Keep only last 100 items
|
452
|
+
|
453
|
+
# Get recent activities
|
454
|
+
recent = feed.activities.range(0, 9) # Get 10 most recent
|
455
|
+
```
|
456
|
+
|
457
|
+
### Hash-Based Configuration Storage
|
458
|
+
Store structured configuration and key-value data.
|
459
|
+
|
460
|
+
```ruby
|
461
|
+
class UserPreferences < Familia::Horreum
|
462
|
+
identifier_field :user_id
|
463
|
+
field :user_id
|
464
|
+
hash :settings
|
465
|
+
hash :feature_flags
|
466
|
+
end
|
467
|
+
|
468
|
+
prefs = UserPreferences.new(user_id: "user123")
|
469
|
+
|
470
|
+
# Set individual preferences
|
471
|
+
prefs.settings.set("theme", "dark")
|
472
|
+
prefs.settings.set("notifications", "true")
|
473
|
+
prefs.settings.set("timezone", "UTC-5")
|
474
|
+
|
475
|
+
# Batch set multiple values
|
476
|
+
prefs.feature_flags.update({
|
477
|
+
"beta_ui" => "true",
|
478
|
+
"new_dashboard" => "false",
|
479
|
+
"advanced_features" => "true"
|
480
|
+
})
|
481
|
+
|
482
|
+
# Get preferences
|
483
|
+
theme = prefs.settings.get("theme") # => "dark"
|
484
|
+
all_settings = prefs.settings.all # => Hash of all settings
|
485
|
+
|
486
|
+
# Check feature flags
|
487
|
+
beta_enabled = prefs.feature_flags.get("beta_ui") == "true"
|
488
|
+
```
|
489
|
+
|
490
|
+
---
|
491
|
+
|
492
|
+
## Error Handling and Validation
|
493
|
+
|
494
|
+
### Connection Error Handling
|
495
|
+
Robust error handling for Redis connection issues.
|
496
|
+
|
497
|
+
```ruby
|
498
|
+
class ResilientService < Familia::Horreum
|
499
|
+
field :name, :data
|
500
|
+
|
501
|
+
def self.with_fallback(&block)
|
502
|
+
retries = 3
|
503
|
+
begin
|
504
|
+
yield
|
505
|
+
rescue Redis::ConnectionError, Redis::TimeoutError => e
|
506
|
+
retries -= 1
|
507
|
+
if retries > 0
|
508
|
+
sleep(0.1 * (4 - retries)) # Exponential backoff
|
509
|
+
retry
|
510
|
+
else
|
511
|
+
Familia.warn "Redis operation failed after retries: #{e.message}"
|
512
|
+
nil # Return nil or handle gracefully
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
def save_with_fallback
|
518
|
+
self.class.with_fallback { save }
|
519
|
+
end
|
520
|
+
end
|
521
|
+
```
|
522
|
+
|
523
|
+
### Data Validation Patterns
|
524
|
+
Implement validation in model classes.
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
class User < Familia::Horreum
|
528
|
+
field :email, :username, :age
|
529
|
+
|
530
|
+
def valid?
|
531
|
+
errors.clear
|
532
|
+
validate_email
|
533
|
+
validate_username
|
534
|
+
validate_age
|
535
|
+
errors.empty?
|
536
|
+
end
|
537
|
+
|
538
|
+
def errors
|
539
|
+
@errors ||= []
|
540
|
+
end
|
541
|
+
|
542
|
+
private
|
543
|
+
|
544
|
+
def validate_email
|
545
|
+
unless email&.include?('@')
|
546
|
+
errors << "Email must be valid"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def validate_username
|
551
|
+
if username.nil? || username.length < 3
|
552
|
+
errors << "Username must be at least 3 characters"
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
def validate_age
|
557
|
+
unless age.is_a?(Integer) && age > 0
|
558
|
+
errors << "Age must be a positive integer"
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
# Usage
|
564
|
+
user = User.new(email: "invalid", username: "ab", age: -5)
|
565
|
+
if user.valid?
|
566
|
+
user.save
|
567
|
+
else
|
568
|
+
puts "Validation errors: #{user.errors.join(', ')}"
|
569
|
+
end
|
570
|
+
```
|
571
|
+
|
572
|
+
---
|
573
|
+
|
574
|
+
## Performance Optimization
|
575
|
+
|
576
|
+
### Batch Operations
|
577
|
+
Minimize Redis round trips with batch operations.
|
578
|
+
|
579
|
+
```ruby
|
580
|
+
# Instead of multiple individual operations
|
581
|
+
users = []
|
582
|
+
100.times do |i|
|
583
|
+
user = User.new(name: "User #{i}", email: "user#{i}@example.com")
|
584
|
+
users << user
|
585
|
+
end
|
586
|
+
|
587
|
+
# Use Redis pipelining for batch saves
|
588
|
+
User.transaction do |redis|
|
589
|
+
users.each do |user|
|
590
|
+
# All operations batched in transaction
|
591
|
+
user.object.set_all(user.to_hash)
|
592
|
+
User.email_index.set(user.email, user.identifier)
|
593
|
+
end
|
594
|
+
end
|
595
|
+
```
|
596
|
+
|
597
|
+
### Memory Optimization
|
598
|
+
Efficient memory usage patterns.
|
599
|
+
|
600
|
+
```ruby
|
601
|
+
class CacheEntry < Familia::Horreum
|
602
|
+
feature :expiration
|
603
|
+
|
604
|
+
field :key, :value, :created_at
|
605
|
+
default_expiration 1.hour
|
606
|
+
|
607
|
+
# Use shorter field names to reduce memory
|
608
|
+
field :k, :v, :c # Instead of key, value, created_at
|
609
|
+
|
610
|
+
# Compress large values
|
611
|
+
def value=(val)
|
612
|
+
@value = val.length > 1000 ? Zlib.deflate(val) : val
|
613
|
+
end
|
614
|
+
|
615
|
+
def value
|
616
|
+
val = @value || ""
|
617
|
+
val.start_with?("\x78\x9c") ? Zlib.inflate(val) : val
|
618
|
+
rescue Zlib::DataError
|
619
|
+
@value # Return original if decompression fails
|
620
|
+
end
|
621
|
+
end
|
622
|
+
```
|
623
|
+
|
624
|
+
### Connection Pool Sizing
|
625
|
+
Configure connection pools based on application needs.
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
# High-throughput application
|
629
|
+
Familia.connection_provider = lambda do |uri|
|
630
|
+
ConnectionPool.new(size: 25, timeout: 5) do
|
631
|
+
Redis.new(uri, connect_timeout: 0.1, read_timeout: 1, write_timeout: 1)
|
632
|
+
end.with { |conn| yield conn if block_given?; conn }
|
633
|
+
end
|
634
|
+
|
635
|
+
# Memory-constrained environment
|
636
|
+
Familia.connection_provider = lambda do |uri|
|
637
|
+
ConnectionPool.new(size: 5, timeout: 10) do
|
638
|
+
Redis.new(uri, connect_timeout: 2, read_timeout: 5, write_timeout: 5)
|
639
|
+
end.with { |conn| yield conn if block_given?; conn }
|
640
|
+
end
|
641
|
+
```
|
642
|
+
|
643
|
+
---
|
644
|
+
|
645
|
+
## Migration and Upgrading
|
646
|
+
|
647
|
+
### From v1.x to v2.0.0-pre
|
648
|
+
Key changes and migration steps.
|
649
|
+
|
650
|
+
```ruby
|
651
|
+
# OLD v1.x syntax
|
652
|
+
class User < Familia
|
653
|
+
identifier :email
|
654
|
+
string :name
|
655
|
+
list :sessions
|
656
|
+
end
|
657
|
+
|
658
|
+
# NEW v2.0.0-pre syntax
|
659
|
+
class User < Familia::Horreum
|
660
|
+
identifier_field :email # Updated method name
|
661
|
+
field :name # Generic field method
|
662
|
+
list :sessions # Data types unchanged
|
663
|
+
end
|
664
|
+
|
665
|
+
# Feature activation (NEW)
|
666
|
+
class User < Familia::Horreum
|
667
|
+
feature :expiration # Explicit feature activation
|
668
|
+
feature :safe_dump
|
669
|
+
|
670
|
+
identifier_field :email
|
671
|
+
field :name
|
672
|
+
list :sessions
|
673
|
+
|
674
|
+
default_expiration 24.hours # Feature-specific methods
|
675
|
+
safe_dump_fields :name # Feature-specific methods
|
676
|
+
end
|
677
|
+
```
|
678
|
+
|
679
|
+
### Encryption Migration
|
680
|
+
Migrating existing fields to encrypted storage.
|
681
|
+
|
682
|
+
```ruby
|
683
|
+
# Step 1: Add feature without changing existing fields
|
684
|
+
class User < Familia::Horreum
|
685
|
+
feature :encrypted_fields # Add feature
|
686
|
+
|
687
|
+
field :name, :email
|
688
|
+
field :api_key # Still plaintext during migration
|
689
|
+
end
|
690
|
+
|
691
|
+
# Step 2: Migrate data with dual read/write
|
692
|
+
class User < Familia::Horreum
|
693
|
+
feature :encrypted_fields
|
694
|
+
|
695
|
+
field :name, :email
|
696
|
+
encrypted_field :api_key # Now encrypted
|
697
|
+
|
698
|
+
# Temporary migration method
|
699
|
+
def migrate_api_key!
|
700
|
+
if raw_api_key = object.get("api_key") # Read old plaintext
|
701
|
+
self.api_key = raw_api_key # Write as encrypted
|
702
|
+
object.delete("api_key") # Remove plaintext
|
703
|
+
save
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
# Step 3: Run migration for all users
|
709
|
+
User.all.each(&:migrate_api_key!)
|
710
|
+
```
|
711
|
+
|
712
|
+
---
|
713
|
+
|
714
|
+
## Testing Patterns
|
715
|
+
|
716
|
+
### Test Helpers and Utilities
|
717
|
+
Common patterns for testing Familia applications.
|
718
|
+
|
719
|
+
```ruby
|
720
|
+
# test_helper.rb
|
721
|
+
require 'familia'
|
722
|
+
|
723
|
+
# Use separate Redis database for tests
|
724
|
+
Familia.configure do |config|
|
725
|
+
config.redis_uri = ENV.fetch('REDIS_TEST_URI', 'redis://localhost:6379/15')
|
726
|
+
end
|
727
|
+
|
728
|
+
module TestHelpers
|
729
|
+
def setup_redis
|
730
|
+
# Clear test database
|
731
|
+
Familia.connection.flushdb
|
732
|
+
end
|
733
|
+
|
734
|
+
def teardown_redis
|
735
|
+
Familia.connection.flushdb
|
736
|
+
end
|
737
|
+
|
738
|
+
def create_test_user(**attrs)
|
739
|
+
User.new({
|
740
|
+
email: "test@example.com",
|
741
|
+
name: "Test User",
|
742
|
+
created_at: Time.now.to_i
|
743
|
+
}.merge(attrs))
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
# In test files
|
748
|
+
class UserTest < Minitest::Test
|
749
|
+
include TestHelpers
|
750
|
+
|
751
|
+
def setup
|
752
|
+
setup_redis
|
753
|
+
end
|
754
|
+
|
755
|
+
def teardown
|
756
|
+
teardown_redis
|
757
|
+
end
|
758
|
+
|
759
|
+
def test_user_creation
|
760
|
+
user = create_test_user(name: "Alice")
|
761
|
+
user.save
|
762
|
+
|
763
|
+
assert user.exists?
|
764
|
+
assert_equal "Alice", user.name
|
765
|
+
end
|
766
|
+
|
767
|
+
def test_relationships
|
768
|
+
user = create_test_user
|
769
|
+
user.save
|
770
|
+
|
771
|
+
domain = Domain.new(domain_id: "test_domain", name: "test.com")
|
772
|
+
domain.save
|
773
|
+
|
774
|
+
# Test relationship
|
775
|
+
domain.add_to_user_domains(user.identifier)
|
776
|
+
assert domain.in_user_domains?(user.identifier)
|
777
|
+
end
|
778
|
+
end
|
779
|
+
```
|
780
|
+
|
781
|
+
---
|
782
|
+
|
783
|
+
## Resources and References
|
784
|
+
|
785
|
+
### Key Configuration
|
786
|
+
Essential configuration options for Familia v2.0.0-pre.
|
787
|
+
|
788
|
+
```ruby
|
789
|
+
Familia.configure do |config|
|
790
|
+
# Basic Redis connection
|
791
|
+
config.redis_uri = ENV['REDIS_URL'] || 'redis://localhost:6379/0'
|
792
|
+
|
793
|
+
# Connection provider for pooling (optional)
|
794
|
+
config.connection_provider = MyConnectionProvider
|
795
|
+
|
796
|
+
# Encryption configuration (for encrypted_fields feature)
|
797
|
+
config.encryption_keys = {
|
798
|
+
v1: ENV['FAMILIA_ENCRYPTION_KEY_V1'],
|
799
|
+
v2: ENV['FAMILIA_ENCRYPTION_KEY_V2']
|
800
|
+
}
|
801
|
+
config.current_key_version = :v2
|
802
|
+
|
803
|
+
# Debugging and logging
|
804
|
+
config.debug = ENV['FAMILIA_DEBUG'] == 'true'
|
805
|
+
config.enable_database_logging = ENV['FAMILIA_LOG_REDIS'] == 'true'
|
806
|
+
end
|
807
|
+
```
|
808
|
+
|
809
|
+
### Documentation Links
|
810
|
+
- [Familia Repository](https://github.com/delano/familia)
|
811
|
+
- [Wiki Home](docs/wiki/Home.md)
|
812
|
+
- [Feature System Guide](docs/wiki/Feature-System-Guide.md)
|
813
|
+
- [Relationships Guide](docs/wiki/Relationships-Guide.md)
|
814
|
+
- [Encrypted Fields Overview](docs/wiki/Encrypted-Fields-Overview.md)
|
815
|
+
- [Connection Pooling Guide](docs/wiki/Connection-Pooling-Guide.md)
|
816
|
+
|
817
|
+
### Version Information
|
818
|
+
- **Current Version**: v2.0.0.pre6 (as of version.rb)
|
819
|
+
- **Target Version**: v2.0.0.pre7 (relationships release)
|
820
|
+
- **Ruby Compatibility**: 3.0+ (3.4+ recommended for optimal threading)
|
821
|
+
- **Redis Compatibility**: 6.0+ (Valkey compatible)
|
822
|
+
|
823
|
+
This technical reference covers the major components and usage patterns available in Familia v2.0.0-pre series. For complete API documentation, see the generated YARD docs and wiki guides.
|