familia 2.0.0.pre26 → 2.1.0

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +94 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +12 -2
  5. data/README.md +1 -3
  6. data/docs/guides/feature-encrypted-fields.md +1 -1
  7. data/docs/guides/feature-expiration.md +1 -1
  8. data/docs/guides/feature-quantization.md +1 -1
  9. data/docs/guides/writing-migrations.md +345 -0
  10. data/docs/overview.md +7 -7
  11. data/docs/reference/api-technical.md +103 -7
  12. data/examples/migrations/v1_to_v2_serialization_migration.rb +374 -0
  13. data/examples/schemas/customer.json +33 -0
  14. data/examples/schemas/session.json +27 -0
  15. data/familia.gemspec +3 -2
  16. data/lib/familia/features/schema_validation.rb +139 -0
  17. data/lib/familia/migration/base.rb +447 -0
  18. data/lib/familia/migration/errors.rb +31 -0
  19. data/lib/familia/migration/model.rb +418 -0
  20. data/lib/familia/migration/pipeline.rb +226 -0
  21. data/lib/familia/migration/rake_tasks.rake +3 -0
  22. data/lib/familia/migration/rake_tasks.rb +160 -0
  23. data/lib/familia/migration/registry.rb +364 -0
  24. data/lib/familia/migration/runner.rb +311 -0
  25. data/lib/familia/migration/script.rb +234 -0
  26. data/lib/familia/migration.rb +43 -0
  27. data/lib/familia/schema_registry.rb +173 -0
  28. data/lib/familia/settings.rb +63 -1
  29. data/lib/familia/version.rb +1 -1
  30. data/lib/familia.rb +1 -0
  31. data/try/features/schema_registry_try.rb +193 -0
  32. data/try/features/schema_validation_feature_try.rb +218 -0
  33. data/try/migration/base_try.rb +226 -0
  34. data/try/migration/errors_try.rb +67 -0
  35. data/try/migration/integration_try.rb +451 -0
  36. data/try/migration/model_try.rb +431 -0
  37. data/try/migration/pipeline_try.rb +460 -0
  38. data/try/migration/rake_tasks_try.rb +61 -0
  39. data/try/migration/registry_try.rb +199 -0
  40. data/try/migration/runner_try.rb +311 -0
  41. data/try/migration/schema_validation_try.rb +201 -0
  42. data/try/migration/script_try.rb +192 -0
  43. data/try/migration/v1_to_v2_serialization_try.rb +513 -0
  44. data/try/performance/benchmarks_try.rb +11 -12
  45. metadata +45 -27
  46. data/docs/migrating/v2.0.0-pre.md +0 -84
  47. data/docs/migrating/v2.0.0-pre11.md +0 -253
  48. data/docs/migrating/v2.0.0-pre12.md +0 -306
  49. data/docs/migrating/v2.0.0-pre13.md +0 -95
  50. data/docs/migrating/v2.0.0-pre14.md +0 -37
  51. data/docs/migrating/v2.0.0-pre18.md +0 -58
  52. data/docs/migrating/v2.0.0-pre19.md +0 -197
  53. data/docs/migrating/v2.0.0-pre22.md +0 -241
  54. data/docs/migrating/v2.0.0-pre5.md +0 -131
  55. data/docs/migrating/v2.0.0-pre6.md +0 -154
  56. data/docs/migrating/v2.0.0-pre7.md +0 -222
@@ -48,7 +48,7 @@ Base class for Valkey/Redis data type implementations.
48
48
 
49
49
  ---
50
50
 
51
- ## Feature System (v2.0.0-pre5+)
51
+ ## Feature System
52
52
 
53
53
  ### Feature Architecture
54
54
  Modular system for extending Horreum classes with reusable functionality.
@@ -500,6 +500,39 @@ Familia::Base.add_feature ExternalIdentifier, :external_identifier, depends_on:
500
500
  end
501
501
  ```
502
502
 
503
+ ### Per-Class Feature Registration
504
+
505
+ Register custom features for specific model classes with ancestry chain lookup.
506
+
507
+ ```ruby
508
+ # Define a custom feature module
509
+ module CustomerAnalytics
510
+ def track_purchase(amount)
511
+ purchases.increment(amount)
512
+ end
513
+ end
514
+
515
+ # Register feature only for Customer and its subclasses
516
+ Customer.add_feature CustomerAnalytics, :customer_analytics
517
+
518
+ class Customer < Familia::Horreum
519
+ feature :customer_analytics # Available via Customer's registry
520
+ end
521
+
522
+ class PremiumCustomer < Customer
523
+ feature :customer_analytics # Inherited via ancestry chain
524
+ end
525
+
526
+ class Session < Familia::Horreum
527
+ # feature :customer_analytics # Not available - would raise error
528
+ end
529
+ ```
530
+
531
+ **Benefits:**
532
+ - Features can have the same name across different model hierarchies
533
+ - Natural inheritance through Ruby's class hierarchy
534
+ - Better namespace management for large applications
535
+
503
536
  ### Per-Class Feature Configuration Isolation
504
537
  Each class maintains independent feature options.
505
538
 
@@ -970,6 +1003,43 @@ end
970
1003
 
971
1004
  ## Performance Optimization
972
1005
 
1006
+ ### Pipelined Bulk Loading
1007
+
1008
+ Load multiple objects efficiently with a single pipelined Redis batch.
1009
+
1010
+ ```ruby
1011
+ # Before: N×2 commands (EXISTS + HGETALL per object)
1012
+ users = ids.map { |id| User.find_by_id(id) }
1013
+ # For 14 objects: 28 Redis commands
1014
+
1015
+ # After: 1 pipelined batch
1016
+ users = User.load_multi(ids)
1017
+ # For 14 objects: 1 batch with 14 HGETALL commands (2× faster)
1018
+
1019
+ # Load by full dbkeys
1020
+ users = User.load_multi_by_keys(['user:123:object', 'user:456:object'])
1021
+
1022
+ # Filter out nils for missing objects
1023
+ existing_users = User.load_multi(ids).compact
1024
+ ```
1025
+
1026
+ ### Optional EXISTS Check Optimization
1027
+
1028
+ Skip the EXISTS check for 50% reduction in Redis commands when keys are known to exist.
1029
+
1030
+ ```ruby
1031
+ # Default behavior (2 commands: EXISTS + HGETALL)
1032
+ user = User.find_by_id(123)
1033
+
1034
+ # Optimized (1 command: HGETALL only)
1035
+ user = User.find_by_id(123, check_exists: false)
1036
+ ```
1037
+
1038
+ **When to use `check_exists: false`:**
1039
+ - Loading from sorted set results (keys guaranteed to exist)
1040
+ - High-throughput API endpoints
1041
+ - Bulk operations with known-existing keys
1042
+
973
1043
  ### Batch Operations
974
1044
  Minimize Valkey/Redis round trips with batch operations.
975
1045
 
@@ -990,6 +1060,33 @@ User.pipelined do
990
1060
  end
991
1061
  ```
992
1062
 
1063
+ ### Index Rebuilding
1064
+
1065
+ Auto-generated rebuild methods for unique and multi indexes with zero downtime.
1066
+
1067
+ ```ruby
1068
+ class User < Familia::Horreum
1069
+ feature :relationships
1070
+ unique_index :email, :email_lookup
1071
+ end
1072
+
1073
+ # Rebuild class-level unique index
1074
+ User.rebuild_email_lookup
1075
+
1076
+ # With progress tracking
1077
+ User.rebuild_email_lookup(batch_size: 100) do |progress|
1078
+ puts "#{progress[:completed]}/#{progress[:total]}"
1079
+ end
1080
+
1081
+ # Instance-scoped index rebuild
1082
+ company.rebuild_badge_index
1083
+ ```
1084
+
1085
+ **When to use:**
1086
+ - After data migrations or bulk imports
1087
+ - Recovering from index corruption
1088
+ - Adding indexes to existing data
1089
+
993
1090
  ### Memory Optimization
994
1091
  Efficient memory usage patterns.
995
1092
 
@@ -1040,7 +1137,7 @@ end
1040
1137
 
1041
1138
  ## Migration and Upgrading
1042
1139
 
1043
- ### From v1.x to v2.0.0-pre
1140
+ ### From v1.x to v2.0
1044
1141
  Key changes and migration steps.
1045
1142
 
1046
1143
  ```ruby
@@ -1051,7 +1148,7 @@ class User < Familia
1051
1148
  list :sessions
1052
1149
  end
1053
1150
 
1054
- # NEW v2.0.0-pre syntax
1151
+ # NEW v2.0 syntax
1055
1152
  class User < Familia::Horreum
1056
1153
  identifier_field :email # Updated method name
1057
1154
  field :name # Generic field method
@@ -1304,9 +1401,8 @@ end
1304
1401
  - [Connection Pooling Guide](../guides/Connection-Pooling-Guide.md)
1305
1402
 
1306
1403
  ### Version Information
1307
- - **Current Version**: v2.0.0.pre6 (as of version.rb)
1308
- - **Target Version**: v2.0.0.pre7 (relationships release)
1309
- - **Ruby Compatibility**: 3.0+ (3.4+ recommended for optimal threading)
1404
+ - **Current Version**: v2.0.0
1405
+ - **Ruby Compatibility**: 3.2+
1310
1406
  - **Redis Compatibility**: 6.0+ (Valkey compatible)
1311
1407
 
1312
- 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.
1408
+ This technical reference covers the major components and usage patterns available in Familia v2.0. For complete API documentation, see the generated YARD docs and wiki guides.
@@ -0,0 +1,374 @@
1
+ # examples/migrations/v1_to_v2_serialization_migration.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ # V1 to V2 Serialization Migration
6
+ #
7
+ # This migration demonstrates how to upgrade Familia Horreum objects from
8
+ # v1.x serialization format (where values were stored as plain strings via
9
+ # distinguisher logic) to v2.0 format (where ALL values are JSON-encoded
10
+ # for type preservation).
11
+ #
12
+ # == Background
13
+ #
14
+ # In Familia v1.x, serialization was selective:
15
+ # - Simple types (String, Integer, Float, Symbol) → stored as plain strings via `to_s`
16
+ # - Booleans → stored as "true"/"false" strings (type info lost)
17
+ # - nil → stored as "" (empty string) or field not present
18
+ # - Hash/Array → JSON encoded
19
+ #
20
+ # In Familia v2.0, serialization is universal:
21
+ # - ALL values → JSON encoded for type preservation
22
+ # - Strings: "hello" → "\"hello\"" (JSON string with quotes)
23
+ # - Integers: 42 → "42" (JSON number, decoded as Integer)
24
+ # - Booleans: true → "true" (JSON boolean, decoded as TrueClass)
25
+ # - nil → "null" (JSON null, decoded as nil)
26
+ #
27
+ # == Migration Strategy
28
+ #
29
+ # This migration:
30
+ # 1. Scans all Horreum object keys for the specified model
31
+ # 2. Reads raw Redis values (bypassing Familia's deserializer)
32
+ # 3. Detects v1.x format values using heuristics
33
+ # 4. Re-serializes values using v2.0 JSON encoding
34
+ # 5. Writes updated values back to Redis
35
+ #
36
+ # == Usage
37
+ #
38
+ # # Create a subclass for your specific model:
39
+ # class CustomerSerializationMigration < V1ToV2SerializationMigration
40
+ # self.migration_id = '20260201_120000_customer_serialization'
41
+ # self.description = 'Migrate Customer model from v1.x to v2.0 serialization'
42
+ #
43
+ # def prepare
44
+ # @model_class = Customer
45
+ # @batch_size = 100
46
+ # super # Important: calls V1ToV2SerializationMigration's prepare
47
+ # end
48
+ # end
49
+ #
50
+ # # Run dry-run first:
51
+ # CustomerSerializationMigration.cli_run
52
+ #
53
+ # # Run actual migration:
54
+ # CustomerSerializationMigration.cli_run(['--run'])
55
+ #
56
+ # == Field Type Declarations
57
+ #
58
+ # For accurate type detection and conversion, override `field_types_for_model`:
59
+ #
60
+ # def field_types_for_model
61
+ # {
62
+ # email: :string,
63
+ # name: :string,
64
+ # age: :integer,
65
+ # balance: :float,
66
+ # active: :boolean,
67
+ # settings: :hash,
68
+ # tags: :array,
69
+ # deleted_at: :timestamp # Integer timestamp
70
+ # }
71
+ # end
72
+ #
73
+ # This helps the migration correctly interpret v1.x values like:
74
+ # - "true" as boolean (not string)
75
+ # - "42" as integer (not string)
76
+ # - "{}" as hash (already JSON, no change needed)
77
+ #
78
+ require_relative '../../lib/familia'
79
+ require_relative '../../lib/familia/migration'
80
+
81
+ class V1ToV2SerializationMigration < Familia::Migration::Model
82
+ self.migration_id = '20260201_000000_v1_to_v2_serialization_base'
83
+ self.description = 'Base migration for v1.x to v2.0 serialization format'
84
+
85
+ # Type mapping for v1.x → v2.0 conversions
86
+ SUPPORTED_TYPES = %i[string integer float boolean hash array timestamp].freeze
87
+
88
+ def prepare
89
+ raise NotImplementedError, "Subclass must set @model_class in #prepare" unless @model_class
90
+
91
+ @batch_size ||= 100
92
+ @field_types = field_types_for_model
93
+
94
+ info "Migrating #{@model_class.name} with field types: #{@field_types.keys.join(', ')}"
95
+ end
96
+
97
+ # Override in subclass to specify field types for your model
98
+ #
99
+ # @return [Hash<Symbol, Symbol>] field_name => type mapping
100
+ # Supported types: :string, :integer, :float, :boolean, :hash, :array, :timestamp
101
+ def field_types_for_model
102
+ # Default: treat all fields as strings (safest, no-op for most)
103
+ # Override in subclass for type-aware conversion
104
+ {}
105
+ end
106
+
107
+ # Override load_from_key to skip Familia's deserialization.
108
+ # For v1→v2 migration, we work directly with raw Redis data.
109
+ # The 'obj' returned is actually just the key itself (a String).
110
+ def load_from_key(key)
111
+ key # Return the key directly, we'll read raw values in process_record
112
+ end
113
+
114
+ def process_record(dbkey, _original_key)
115
+ # Note: dbkey is the key string (not an object) because we override load_from_key
116
+ # Read raw Redis values (bypass Familia deserialization)
117
+ raw_values = read_raw_values(dbkey)
118
+
119
+ return track_stat(:empty_records) if raw_values.empty?
120
+
121
+ # Detect and convert v1.x values to v2.0 format
122
+ converted = convert_v1_to_v2(raw_values)
123
+
124
+ if converted.empty?
125
+ debug "No fields need conversion for #{dbkey}"
126
+ track_stat(:already_v2_format)
127
+ return
128
+ end
129
+
130
+ debug "Converting #{converted.size} fields for #{dbkey}: #{converted.keys.join(', ')}"
131
+
132
+ for_realsies_this_time? do
133
+ write_converted_values(dbkey, converted)
134
+ end
135
+
136
+ track_stat(:records_updated)
137
+ track_stat(:fields_converted, converted.size)
138
+ end
139
+
140
+ protected
141
+
142
+ # Read raw string values from Redis, bypassing Familia's deserializer
143
+ def read_raw_values(dbkey)
144
+ redis.hgetall(dbkey)
145
+ end
146
+
147
+ # Write converted values back to Redis using HMSET
148
+ def write_converted_values(dbkey, converted)
149
+ redis.hmset(dbkey, *converted.flatten) if converted.any?
150
+ end
151
+
152
+ # Convert v1.x format values to v2.0 JSON-encoded format
153
+ #
154
+ # @param raw_values [Hash<String, String>] field_name => raw Redis value
155
+ # @return [Hash<String, String>] field_name => converted JSON value
156
+ def convert_v1_to_v2(raw_values)
157
+ converted = {}
158
+
159
+ raw_values.each do |field_name, raw_value|
160
+ field_sym = field_name.to_sym
161
+ field_type = @field_types[field_sym] || detect_type(raw_value)
162
+
163
+ # Skip if already in v2.0 format
164
+ next if already_v2_format?(raw_value, field_type)
165
+
166
+ # Convert v1.x value to v2.0 format
167
+ v2_value = convert_value(raw_value, field_type)
168
+
169
+ if v2_value != raw_value
170
+ converted[field_name] = v2_value
171
+ track_stat("converted_#{field_type}".to_sym)
172
+ end
173
+ end
174
+
175
+ converted
176
+ end
177
+
178
+ # Detect if a value is already in v2.0 JSON format
179
+ #
180
+ # v2.0 format characteristics:
181
+ # - Strings are JSON-quoted: "\"hello\""
182
+ # - Numbers, booleans are valid JSON: "42", "true", "false"
183
+ # - null is explicit: "null"
184
+ # - Hashes/Arrays are JSON objects/arrays: "{...}", "[...]"
185
+ #
186
+ # v1.x format characteristics:
187
+ # - Strings are plain: "hello" (no wrapping quotes)
188
+ # - Numbers stored as string but parsed same as JSON
189
+ # - Booleans same as JSON but interpreted as strings
190
+ # - Empty string "" for nil (v2 uses "null")
191
+ def already_v2_format?(value, expected_type)
192
+ # nil values in Ruby don't need conversion (handled elsewhere)
193
+ return true if value.nil?
194
+
195
+ # Empty strings in v1.x represent nil, which should be "null" in v2.0
196
+ # So empty strings are NOT already in v2.0 format
197
+ return false if value.empty?
198
+
199
+ case expected_type
200
+ when :string
201
+ # v2.0 strings start and end with escaped quotes
202
+ value.start_with?('"') && value.end_with?('"')
203
+
204
+ when :integer, :float
205
+ # Numbers look the same in both formats, but v2 JSON parses correctly
206
+ # Can't reliably detect, so we'll skip if parseable as JSON number
207
+ begin
208
+ parsed = Familia::JsonSerializer.parse(value)
209
+ parsed.is_a?(Integer) || parsed.is_a?(Float)
210
+ rescue Familia::SerializerError
211
+ false
212
+ end
213
+
214
+ when :boolean
215
+ # Both formats store "true"/"false", but v1 parses as string
216
+ # v2 parses as actual boolean - can't detect from storage alone
217
+ # We need to re-serialize to ensure correct JSON format
218
+ value == 'true' || value == 'false'
219
+
220
+ when :hash, :array
221
+ # Both v1 and v2 store as JSON, already compatible
222
+ begin
223
+ parsed = Familia::JsonSerializer.parse(value)
224
+ (expected_type == :hash && parsed.is_a?(Hash)) ||
225
+ (expected_type == :array && parsed.is_a?(Array))
226
+ rescue Familia::SerializerError
227
+ false
228
+ end
229
+
230
+ when :timestamp
231
+ # Timestamps are integers, same handling as :integer
232
+ already_v2_format?(value, :integer)
233
+
234
+ else
235
+ false
236
+ end
237
+ end
238
+
239
+ # Convert a v1.x value to v2.0 JSON-encoded format
240
+ #
241
+ # @param raw_value [String] The raw Redis string value
242
+ # @param field_type [Symbol] Expected field type
243
+ # @return [String] JSON-encoded value for v2.0 storage
244
+ def convert_value(raw_value, field_type)
245
+ # Handle empty string (v1.x nil representation)
246
+ return 'null' if raw_value == ''
247
+
248
+ ruby_value = parse_v1_value(raw_value, field_type)
249
+ Familia::JsonSerializer.dump(ruby_value)
250
+ rescue StandardError => e
251
+ warn "Failed to convert value '#{raw_value}' as #{field_type}: #{e.message}"
252
+ track_stat(:conversion_errors)
253
+ raw_value # Return original on error
254
+ end
255
+
256
+ # Parse a v1.x stored value to its Ruby type
257
+ #
258
+ # @param raw_value [String] The raw Redis string value
259
+ # @param field_type [Symbol] Expected field type
260
+ # @return [Object] The parsed Ruby value
261
+ def parse_v1_value(raw_value, field_type)
262
+ case field_type
263
+ when :string
264
+ # v1 strings are stored as-is, already correct Ruby type
265
+ raw_value
266
+
267
+ when :integer, :timestamp
268
+ # v1 integers stored as string "42"
269
+ raw_value.to_i
270
+
271
+ when :float
272
+ # v1 floats stored as string "3.14"
273
+ raw_value.to_f
274
+
275
+ when :boolean
276
+ # v1 booleans stored as "true"/"false" strings
277
+ raw_value == 'true'
278
+
279
+ when :hash, :array
280
+ # v1 complex types already JSON-encoded, parse them
281
+ begin
282
+ Familia::JsonSerializer.parse(raw_value)
283
+ rescue Familia::SerializerError
284
+ # Corrupted JSON, return empty structure
285
+ field_type == :hash ? {} : []
286
+ end
287
+
288
+ else
289
+ # Unknown type, treat as string
290
+ raw_value
291
+ end
292
+ end
293
+
294
+ # Attempt to detect the type of a value from its format
295
+ #
296
+ # Used when field_types_for_model doesn't specify a field type.
297
+ # This is a heuristic and may not always be accurate.
298
+ #
299
+ # @param value [String] The raw Redis value
300
+ # @return [Symbol] Detected type (defaults to :string)
301
+ def detect_type(value)
302
+ return :string if value.nil? || value.empty?
303
+
304
+ # JSON object (hash)
305
+ return :hash if value.start_with?('{') && value.end_with?('}')
306
+
307
+ # JSON array
308
+ return :array if value.start_with?('[') && value.end_with?(']')
309
+
310
+ # Potential boolean
311
+ return :boolean if %w[true false].include?(value)
312
+
313
+ # JSON null (v2.0 format for nil)
314
+ return :string if value == 'null'
315
+
316
+ # Potential integer
317
+ return :integer if value.match?(/\A-?\d+\z/)
318
+
319
+ # Potential float
320
+ return :float if value.match?(/\A-?\d+\.\d+\z/)
321
+
322
+ # Already JSON-quoted string (v2.0 format)
323
+ return :string if value.start_with?('"') && value.end_with?('"')
324
+
325
+ # Default: plain string (v1.x format)
326
+ :string
327
+ end
328
+ end
329
+
330
+ # Example: Concrete migration for a User model
331
+ #
332
+ # Uncomment and customize for your application:
333
+ #
334
+ # class UserSerializationMigration < V1ToV2SerializationMigration
335
+ # self.migration_id = '20260201_120000_user_serialization'
336
+ # self.description = 'Migrate User model from v1.x to v2.0 serialization'
337
+ #
338
+ # def prepare
339
+ # @model_class = User
340
+ # @batch_size = 100
341
+ # super
342
+ # end
343
+ #
344
+ # def field_types_for_model
345
+ # {
346
+ # email: :string,
347
+ # name: :string,
348
+ # age: :integer,
349
+ # balance: :float,
350
+ # active: :boolean,
351
+ # verified: :boolean,
352
+ # login_count: :integer,
353
+ # last_login_at: :timestamp,
354
+ # settings: :hash,
355
+ # roles: :array
356
+ # }
357
+ # end
358
+ # end
359
+
360
+ if $PROGRAM_NAME == __FILE__
361
+ puts "V1ToV2SerializationMigration is a base class."
362
+ puts "Create a subclass for your specific model and run that."
363
+ puts
364
+ puts "Example:"
365
+ puts " class CustomerMigration < V1ToV2SerializationMigration"
366
+ puts " self.migration_id = '20260201_customer_v2'"
367
+ puts " def prepare"
368
+ puts " @model_class = Customer"
369
+ puts " super"
370
+ puts " end"
371
+ puts " end"
372
+ puts
373
+ puts " CustomerMigration.cli_run"
374
+ end
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://example.com/schemas/customer.json",
4
+ "title": "Customer",
5
+ "description": "Customer model schema for Familia ORM",
6
+ "type": "object",
7
+ "properties": {
8
+ "custid": {
9
+ "type": "string",
10
+ "description": "Unique customer identifier"
11
+ },
12
+ "email": {
13
+ "type": "string",
14
+ "format": "email",
15
+ "description": "Customer email address"
16
+ },
17
+ "name": {
18
+ "type": "string",
19
+ "minLength": 1,
20
+ "maxLength": 255
21
+ },
22
+ "status": {
23
+ "type": "string",
24
+ "enum": ["active", "inactive", "pending"],
25
+ "default": "pending"
26
+ },
27
+ "created_at": {
28
+ "type": "number",
29
+ "description": "Unix timestamp"
30
+ }
31
+ },
32
+ "required": ["custid", "email"]
33
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://example.com/schemas/session.json",
4
+ "title": "Session",
5
+ "description": "User session model schema",
6
+ "type": "object",
7
+ "properties": {
8
+ "sessid": {
9
+ "type": "string",
10
+ "pattern": "^[a-f0-9]{32}$",
11
+ "description": "32-character hex session ID"
12
+ },
13
+ "customer_id": {
14
+ "type": "string"
15
+ },
16
+ "expires_at": {
17
+ "type": "number",
18
+ "description": "Unix timestamp when session expires"
19
+ },
20
+ "data": {
21
+ "type": "object",
22
+ "additionalProperties": true,
23
+ "description": "Arbitrary session data"
24
+ }
25
+ },
26
+ "required": ["sessid"]
27
+ }
data/familia.gemspec CHANGED
@@ -17,9 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
18
  spec.require_paths = ['lib']
19
19
 
20
- spec.required_ruby_version = Gem::Requirement.new('>= 3.3.6')
20
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.2')
21
21
 
22
- spec.add_dependency 'benchmark', '~> 0.4'
23
22
  spec.add_dependency 'concurrent-ruby', '~> 1.3'
24
23
  spec.add_dependency 'connection_pool', '~> 2.5'
25
24
  spec.add_dependency 'csv', '~> 3.3'
@@ -29,5 +28,7 @@ Gem::Specification.new do |spec|
29
28
  spec.add_dependency 'stringio', '~> 3.1.1'
30
29
  spec.add_dependency 'uri-valkey', '~> 1.4'
31
30
 
31
+ spec.add_development_dependency 'json_schemer', '~> 2.0'
32
+
32
33
  spec.metadata['rubygems_mfa_required'] = 'true'
33
34
  end