familia 2.0.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +45 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +11 -1
- data/docs/guides/writing-migrations.md +345 -0
- data/examples/migrations/v1_to_v2_serialization_migration.rb +374 -0
- data/examples/schemas/customer.json +33 -0
- data/examples/schemas/session.json +27 -0
- data/familia.gemspec +2 -0
- data/lib/familia/data_type/types/hashkey.rb +0 -238
- data/lib/familia/data_type/types/listkey.rb +4 -110
- data/lib/familia/data_type/types/sorted_set.rb +0 -365
- data/lib/familia/data_type/types/stringkey.rb +0 -139
- data/lib/familia/data_type/types/unsorted_set.rb +2 -122
- data/lib/familia/features/schema_validation.rb +139 -0
- data/lib/familia/migration/base.rb +447 -0
- data/lib/familia/migration/errors.rb +31 -0
- data/lib/familia/migration/model.rb +418 -0
- data/lib/familia/migration/pipeline.rb +226 -0
- data/lib/familia/migration/rake_tasks.rake +3 -0
- data/lib/familia/migration/rake_tasks.rb +160 -0
- data/lib/familia/migration/registry.rb +364 -0
- data/lib/familia/migration/runner.rb +311 -0
- data/lib/familia/migration/script.rb +234 -0
- data/lib/familia/migration.rb +43 -0
- data/lib/familia/schema_registry.rb +173 -0
- data/lib/familia/settings.rb +63 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -0
- data/try/features/schema_registry_try.rb +193 -0
- data/try/features/schema_validation_feature_try.rb +218 -0
- data/try/migration/base_try.rb +226 -0
- data/try/migration/errors_try.rb +67 -0
- data/try/migration/integration_try.rb +451 -0
- data/try/migration/model_try.rb +431 -0
- data/try/migration/pipeline_try.rb +460 -0
- data/try/migration/rake_tasks_try.rb +61 -0
- data/try/migration/registry_try.rb +199 -0
- data/try/migration/runner_try.rb +311 -0
- data/try/migration/schema_validation_try.rb +201 -0
- data/try/migration/script_try.rb +192 -0
- data/try/migration/v1_to_v2_serialization_try.rb +513 -0
- data/try/performance/benchmarks_try.rb +11 -12
- metadata +44 -1
|
@@ -87,113 +87,9 @@ module Familia
|
|
|
87
87
|
end
|
|
88
88
|
alias remove remove_element # deprecated
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# @return [Array] Deserialized members present in all sets
|
|
93
|
-
def intersection(*other_sets)
|
|
94
|
-
keys = extract_keys(other_sets)
|
|
95
|
-
elements = dbclient.sinter(dbkey, *keys)
|
|
96
|
-
deserialize_values(*elements)
|
|
97
|
-
end
|
|
98
|
-
alias inter intersection
|
|
99
|
-
|
|
100
|
-
# Returns the union of this set with one or more other sets.
|
|
101
|
-
# @param other_sets [Array<UnsortedSet, String>] Other sets (as UnsortedSet instances or raw keys)
|
|
102
|
-
# @return [Array] Deserialized members present in any of the sets
|
|
103
|
-
def union(*other_sets)
|
|
104
|
-
keys = extract_keys(other_sets)
|
|
105
|
-
elements = dbclient.sunion(dbkey, *keys)
|
|
106
|
-
deserialize_values(*elements)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Returns the difference of this set minus one or more other sets.
|
|
110
|
-
# @param other_sets [Array<UnsortedSet, String>] Other sets (as UnsortedSet instances or raw keys)
|
|
111
|
-
# @return [Array] Deserialized members present in this set but not in any other sets
|
|
112
|
-
def difference(*other_sets)
|
|
113
|
-
keys = extract_keys(other_sets)
|
|
114
|
-
elements = dbclient.sdiff(dbkey, *keys)
|
|
115
|
-
deserialize_values(*elements)
|
|
116
|
-
end
|
|
117
|
-
alias diff difference
|
|
118
|
-
|
|
119
|
-
# Checks membership for multiple values at once.
|
|
120
|
-
# @param values [Array] Values to check for membership
|
|
121
|
-
# @return [Array<Boolean>] Array of booleans indicating membership for each value
|
|
122
|
-
def member_any?(*values)
|
|
123
|
-
values = values.flatten
|
|
124
|
-
serialized = values.map { |v| serialize_value(v) }
|
|
125
|
-
dbclient.smismember(dbkey, *serialized)
|
|
126
|
-
end
|
|
127
|
-
alias members? member_any?
|
|
128
|
-
|
|
129
|
-
# Iterates over set members using cursor-based iteration.
|
|
130
|
-
# @param cursor [Integer] Starting cursor position (default: 0)
|
|
131
|
-
# @param match [String, nil] Optional pattern to filter members
|
|
132
|
-
# @param count [Integer, nil] Optional hint for number of elements to return per call
|
|
133
|
-
# @return [Array<Integer, Array>] Two-element array: [new_cursor, deserialized_members]
|
|
134
|
-
def scan(cursor = 0, match: nil, count: nil)
|
|
135
|
-
opts = {}
|
|
136
|
-
opts[:match] = match if match
|
|
137
|
-
opts[:count] = count if count
|
|
138
|
-
|
|
139
|
-
new_cursor, elements = dbclient.sscan(dbkey, cursor, **opts)
|
|
140
|
-
[new_cursor.to_i, deserialize_values(*elements)]
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Returns the cardinality of the intersection without retrieving members.
|
|
144
|
-
# More memory-efficient than intersection when only the count is needed.
|
|
145
|
-
# @param other_sets [Array<UnsortedSet, String>] Other sets (as UnsortedSet instances or raw keys)
|
|
146
|
-
# @param limit [Integer] Stop counting after reaching this limit (0 = no limit)
|
|
147
|
-
# @return [Integer] Number of elements in the intersection
|
|
148
|
-
def intercard(*other_sets, limit: 0)
|
|
149
|
-
keys = extract_keys(other_sets)
|
|
150
|
-
all_keys = [dbkey, *keys]
|
|
151
|
-
if limit.positive?
|
|
152
|
-
dbclient.sintercard(all_keys.size, *all_keys, limit: limit)
|
|
153
|
-
else
|
|
154
|
-
dbclient.sintercard(all_keys.size, *all_keys)
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
alias intersection_cardinality intercard
|
|
158
|
-
|
|
159
|
-
# Stores the intersection of this set with other sets into a destination key.
|
|
160
|
-
# @param destination [UnsortedSet, String] Destination set (as UnsortedSet instance or raw key)
|
|
161
|
-
# @param other_sets [Array<UnsortedSet, String>] Other sets to intersect with
|
|
162
|
-
# @return [Integer] Number of elements in the resulting set
|
|
163
|
-
def interstore(destination, *other_sets)
|
|
164
|
-
dest_key = extract_key(destination)
|
|
165
|
-
keys = extract_keys(other_sets)
|
|
166
|
-
result = dbclient.sinterstore(dest_key, dbkey, *keys)
|
|
167
|
-
update_expiration
|
|
168
|
-
result
|
|
169
|
-
end
|
|
170
|
-
alias intersection_store interstore
|
|
171
|
-
|
|
172
|
-
# Stores the union of this set with other sets into a destination key.
|
|
173
|
-
# @param destination [UnsortedSet, String] Destination set (as UnsortedSet instance or raw key)
|
|
174
|
-
# @param other_sets [Array<UnsortedSet, String>] Other sets to union with
|
|
175
|
-
# @return [Integer] Number of elements in the resulting set
|
|
176
|
-
def unionstore(destination, *other_sets)
|
|
177
|
-
dest_key = extract_key(destination)
|
|
178
|
-
keys = extract_keys(other_sets)
|
|
179
|
-
result = dbclient.sunionstore(dest_key, dbkey, *keys)
|
|
180
|
-
update_expiration
|
|
181
|
-
result
|
|
182
|
-
end
|
|
183
|
-
alias union_store unionstore
|
|
184
|
-
|
|
185
|
-
# Stores the difference of this set minus other sets into a destination key.
|
|
186
|
-
# @param destination [UnsortedSet, String] Destination set (as UnsortedSet instance or raw key)
|
|
187
|
-
# @param other_sets [Array<UnsortedSet, String>] Other sets to subtract
|
|
188
|
-
# @return [Integer] Number of elements in the resulting set
|
|
189
|
-
def diffstore(destination, *other_sets)
|
|
190
|
-
dest_key = extract_key(destination)
|
|
191
|
-
keys = extract_keys(other_sets)
|
|
192
|
-
result = dbclient.sdiffstore(dest_key, dbkey, *keys)
|
|
193
|
-
update_expiration
|
|
194
|
-
result
|
|
90
|
+
def intersection *setkeys
|
|
91
|
+
# TODO
|
|
195
92
|
end
|
|
196
|
-
alias difference_store diffstore
|
|
197
93
|
|
|
198
94
|
def pop
|
|
199
95
|
dbclient.spop dbkey
|
|
@@ -219,22 +115,6 @@ module Familia
|
|
|
219
115
|
end
|
|
220
116
|
alias random sampleraw
|
|
221
117
|
|
|
222
|
-
private
|
|
223
|
-
|
|
224
|
-
# Extracts the database key from a set reference.
|
|
225
|
-
# @param set_ref [UnsortedSet, String] An UnsortedSet instance or raw key string
|
|
226
|
-
# @return [String] The database key
|
|
227
|
-
def extract_key(set_ref)
|
|
228
|
-
set_ref.respond_to?(:dbkey) ? set_ref.dbkey : set_ref.to_s
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Extracts database keys from an array of set references.
|
|
232
|
-
# @param set_refs [Array<UnsortedSet, String>] Array of UnsortedSet instances or raw keys
|
|
233
|
-
# @return [Array<String>] Array of database keys
|
|
234
|
-
def extract_keys(set_refs)
|
|
235
|
-
set_refs.flatten.map { |s| extract_key(s) }
|
|
236
|
-
end
|
|
237
|
-
|
|
238
118
|
Familia::DataType.register self, :set
|
|
239
119
|
Familia::DataType.register self, :unsorted_set
|
|
240
120
|
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# lib/familia/features/schema_validation.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module Familia
|
|
6
|
+
module Features
|
|
7
|
+
# Adds JSON schema validation methods to Horreum models.
|
|
8
|
+
# Schemas are loaded from external files via SchemaRegistry.
|
|
9
|
+
#
|
|
10
|
+
# This feature provides instance-level validation against JSON schemas,
|
|
11
|
+
# enabling data integrity checks before persistence or during API operations.
|
|
12
|
+
#
|
|
13
|
+
# Example:
|
|
14
|
+
#
|
|
15
|
+
# class Customer < Familia::Horreum
|
|
16
|
+
# feature :schema_validation
|
|
17
|
+
# identifier_field :custid
|
|
18
|
+
# field :custid
|
|
19
|
+
# field :email
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # With schema at schemas/customer.json
|
|
23
|
+
# customer = Customer.new(custid: 'c1', email: 'invalid')
|
|
24
|
+
# customer.valid_against_schema? # => false
|
|
25
|
+
# customer.schema_validation_errors # => [...]
|
|
26
|
+
# customer.validate_against_schema! # raises SchemaValidationError
|
|
27
|
+
#
|
|
28
|
+
# Schema Loading:
|
|
29
|
+
#
|
|
30
|
+
# Schemas are loaded by SchemaRegistry based on class names. Configure
|
|
31
|
+
# schema loading before enabling this feature:
|
|
32
|
+
#
|
|
33
|
+
# # Convention-based loading
|
|
34
|
+
# Familia.schema_path = 'schemas/models'
|
|
35
|
+
# # Loads schemas/models/customer.json for Customer class
|
|
36
|
+
#
|
|
37
|
+
# # Explicit mapping
|
|
38
|
+
# Familia.schemas = { 'Customer' => 'schemas/customer.json' }
|
|
39
|
+
#
|
|
40
|
+
# Validation Behavior:
|
|
41
|
+
#
|
|
42
|
+
# - Returns true/valid if no schema is defined for the class
|
|
43
|
+
# - Uses json_schemer gem for validation when available
|
|
44
|
+
# - Falls back to null validation (always passes) if gem not installed
|
|
45
|
+
#
|
|
46
|
+
# Integration Patterns:
|
|
47
|
+
#
|
|
48
|
+
# # Validate before save
|
|
49
|
+
# class Order < Familia::Horreum
|
|
50
|
+
# feature :schema_validation
|
|
51
|
+
#
|
|
52
|
+
# def save
|
|
53
|
+
# validate_against_schema!
|
|
54
|
+
# super
|
|
55
|
+
# end
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# # Conditional validation
|
|
59
|
+
# class User < Familia::Horreum
|
|
60
|
+
# feature :schema_validation
|
|
61
|
+
#
|
|
62
|
+
# def save
|
|
63
|
+
# if self.class.schema_defined?
|
|
64
|
+
# return false unless valid_against_schema?
|
|
65
|
+
# end
|
|
66
|
+
# super
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# Error Handling:
|
|
71
|
+
#
|
|
72
|
+
# The validate_against_schema! method raises SchemaValidationError with
|
|
73
|
+
# detailed error information:
|
|
74
|
+
#
|
|
75
|
+
# begin
|
|
76
|
+
# customer.validate_against_schema!
|
|
77
|
+
# rescue Familia::SchemaValidationError => e
|
|
78
|
+
# e.errors # => [{ 'data_pointer' => '/email', 'type' => 'format', ... }]
|
|
79
|
+
# end
|
|
80
|
+
#
|
|
81
|
+
# @see Familia::SchemaRegistry for schema loading and configuration
|
|
82
|
+
# @see Familia::SchemaValidationError for error details
|
|
83
|
+
#
|
|
84
|
+
module SchemaValidation
|
|
85
|
+
Familia::Base.add_feature self, :schema_validation
|
|
86
|
+
|
|
87
|
+
def self.included(base)
|
|
88
|
+
Familia.trace :LOADED, self, base if Familia.debug?
|
|
89
|
+
base.extend(ClassMethods)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Class-level schema access methods
|
|
93
|
+
module ClassMethods
|
|
94
|
+
# Get the JSON schema for this class
|
|
95
|
+
# @return [Hash, nil] the parsed schema or nil if not defined
|
|
96
|
+
def schema
|
|
97
|
+
Familia::SchemaRegistry.schema_for(name)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if a schema is defined for this class
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def schema_defined?
|
|
103
|
+
Familia::SchemaRegistry.schema_defined?(name)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get the schema for this instance's class
|
|
108
|
+
# @return [Hash, nil]
|
|
109
|
+
def schema
|
|
110
|
+
self.class.schema
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if the current state validates against the schema
|
|
114
|
+
# @return [Boolean] true if valid or no schema defined
|
|
115
|
+
def valid_against_schema?
|
|
116
|
+
return true unless self.class.schema_defined?
|
|
117
|
+
|
|
118
|
+
Familia::SchemaRegistry.validate(self.class.name, to_h)[:valid]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Get validation errors for the current state
|
|
122
|
+
# @return [Array<Hash>] array of error objects (empty if valid)
|
|
123
|
+
def schema_validation_errors
|
|
124
|
+
return [] unless self.class.schema_defined?
|
|
125
|
+
|
|
126
|
+
Familia::SchemaRegistry.validate(self.class.name, to_h)[:errors]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Validate current state or raise SchemaValidationError
|
|
130
|
+
# @return [true] if valid
|
|
131
|
+
# @raise [SchemaValidationError] if validation fails
|
|
132
|
+
def validate_against_schema!
|
|
133
|
+
return true unless self.class.schema_defined?
|
|
134
|
+
|
|
135
|
+
Familia::SchemaRegistry.validate!(self.class.name, to_h)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|