familia 2.0.0.pre5 → 2.0.0.pre7
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/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,442 @@
|
|
1
|
+
# lib/familia/features/relationships/score_encoding.rb
|
2
|
+
|
3
|
+
module Familia
|
4
|
+
module Features
|
5
|
+
module Relationships
|
6
|
+
# Score encoding using bit flags for permissions
|
7
|
+
#
|
8
|
+
# Encodes permissions as bit flags in the decimal portion of Redis sorted set scores:
|
9
|
+
# - Integer part: Unix timestamp for time-based ordering
|
10
|
+
# - Decimal part: 8-bit permission flags (0-255)
|
11
|
+
#
|
12
|
+
# Format: [timestamp].[permission_bits]
|
13
|
+
# Example: 1704067200.037 = Jan 1, 2024 with read(1) + write(4) + delete(32) = 37
|
14
|
+
#
|
15
|
+
# Bit positions:
|
16
|
+
# 0: read - View/list items
|
17
|
+
# 1: append - Add new items
|
18
|
+
# 2: write - Modify existing items
|
19
|
+
# 3: edit - Edit metadata
|
20
|
+
# 4: configure - Change settings
|
21
|
+
# 5: delete - Remove items
|
22
|
+
# 6: transfer - Change ownership
|
23
|
+
# 7: admin - Full control
|
24
|
+
#
|
25
|
+
# This allows combining permissions (read + delete without write) and efficient
|
26
|
+
# permission checking using bitwise operations while maintaining time-based ordering.
|
27
|
+
module ScoreEncoding
|
28
|
+
# Maximum value for metadata to preserve precision (3 decimal places)
|
29
|
+
# For 8-bit permission system, max value is 255
|
30
|
+
MAX_METADATA = 255
|
31
|
+
METADATA_PRECISION = 1000.0
|
32
|
+
|
33
|
+
# Permission bit flags (8-bit system)
|
34
|
+
PERMISSION_FLAGS = {
|
35
|
+
none: 0b00000000, # 0 - No permissions
|
36
|
+
read: 0b00000001, # 1 - View/list
|
37
|
+
append: 0b00000010, # 2 - Add new items
|
38
|
+
write: 0b00000100, # 4 - Modify existing
|
39
|
+
edit: 0b00001000, # 8 - Edit metadata
|
40
|
+
configure: 0b00010000, # 16 - Change settings
|
41
|
+
delete: 0b00100000, # 32 - Remove items
|
42
|
+
transfer: 0b01000000, # 64 - Change ownership
|
43
|
+
admin: 0b10000000 # 128 - Full control
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
# Predefined permission combinations
|
47
|
+
PERMISSION_ROLES = {
|
48
|
+
viewer: PERMISSION_FLAGS[:read],
|
49
|
+
editor: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit],
|
50
|
+
moderator: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit] | PERMISSION_FLAGS[:delete],
|
51
|
+
admin: 0b11111111 # All permissions
|
52
|
+
}.freeze
|
53
|
+
|
54
|
+
# Categorical masks for efficient broad queries
|
55
|
+
PERMISSION_CATEGORIES = {
|
56
|
+
readable: 0b00000001, # Has basic access
|
57
|
+
content_editor: 0b00001110, # Can modify content (append|write|edit)
|
58
|
+
administrator: 0b11110000, # Has any admin powers
|
59
|
+
privileged: 0b11111110, # Has beyond read-only
|
60
|
+
owner: 0b11111111 # All permissions
|
61
|
+
}.freeze
|
62
|
+
|
63
|
+
|
64
|
+
class << self
|
65
|
+
# Get permission bit flag value for a permission symbol
|
66
|
+
#
|
67
|
+
# @param permission [Symbol] Permission symbol to get value for
|
68
|
+
# @return [Integer] Bit flag value for the permission
|
69
|
+
# @raise [ArgumentError] If permission is unknown
|
70
|
+
def permission_level_value(permission)
|
71
|
+
PERMISSION_FLAGS[permission] || raise(ArgumentError, "Unknown permission: #{permission.inspect}")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Encode timestamp and permission (alias for encode_score)
|
75
|
+
#
|
76
|
+
# @param timestamp [Time, Integer] The timestamp to encode
|
77
|
+
# @param permission [Symbol, Integer, Array] Permission(s) to encode
|
78
|
+
# @return [Float] Encoded score suitable for Redis sorted sets
|
79
|
+
def permission_encode(timestamp, permission)
|
80
|
+
encode_score(timestamp, permission)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Decode score into permission information
|
84
|
+
#
|
85
|
+
# @param score [Float] The encoded score
|
86
|
+
# @return [Hash] Hash with timestamp, permissions bits, and permission list
|
87
|
+
def permission_decode(score)
|
88
|
+
decoded = decode_score(score)
|
89
|
+
{
|
90
|
+
timestamp: decoded[:timestamp],
|
91
|
+
permissions: decoded[:permissions],
|
92
|
+
permission_list: decoded[:permission_list]
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# Encode a timestamp and permissions into a Redis score
|
98
|
+
#
|
99
|
+
# @param timestamp [Time, Integer] The timestamp to encode
|
100
|
+
# @param permissions [Integer, Symbol, Array] Permissions to encode
|
101
|
+
# @return [Float] Encoded score suitable for Redis sorted sets
|
102
|
+
#
|
103
|
+
# @example Basic encoding with bit flag
|
104
|
+
# encode_score(Time.now, 5) # read(1) + write(4) = 5
|
105
|
+
# #=> 1704067200.005
|
106
|
+
#
|
107
|
+
# @example Permission symbol encoding
|
108
|
+
# encode_score(Time.now, :read)
|
109
|
+
# #=> 1704067200.001
|
110
|
+
#
|
111
|
+
# @example Multiple permissions
|
112
|
+
# encode_score(Time.now, [:read, :write, :delete])
|
113
|
+
# #=> 1704067200.037
|
114
|
+
def encode_score(timestamp, permissions = 0)
|
115
|
+
time_part = timestamp.respond_to?(:to_i) ? timestamp.to_i : timestamp
|
116
|
+
|
117
|
+
permission_bits = case permissions
|
118
|
+
when Symbol
|
119
|
+
PERMISSION_ROLES[permissions] || PERMISSION_FLAGS[permissions] || 0
|
120
|
+
when Array
|
121
|
+
# Support array of permission symbols
|
122
|
+
permissions.reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) }
|
123
|
+
when Integer
|
124
|
+
validate_permission_bits(permissions)
|
125
|
+
else
|
126
|
+
0
|
127
|
+
end
|
128
|
+
|
129
|
+
time_part + (permission_bits / METADATA_PRECISION)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Decode a Redis score back into timestamp and permissions
|
133
|
+
#
|
134
|
+
# @param score [Float] The encoded score
|
135
|
+
# @return [Hash] Hash with :timestamp, :permissions, and :permission_list keys
|
136
|
+
#
|
137
|
+
# @example Basic decoding
|
138
|
+
# decode_score(1704067200.037)
|
139
|
+
# #=> { timestamp: 1704067200, permissions: 37, permission_list: [:read, :write, :delete] }
|
140
|
+
def decode_score(score)
|
141
|
+
return { timestamp: 0, permissions: 0, permission_list: [] } unless score.is_a?(Numeric)
|
142
|
+
|
143
|
+
time_part = score.to_i
|
144
|
+
permission_bits = ((score - time_part) * METADATA_PRECISION).round
|
145
|
+
|
146
|
+
{
|
147
|
+
timestamp: time_part,
|
148
|
+
permissions: permission_bits,
|
149
|
+
permission_list: decode_permission_flags(permission_bits)
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
# Check if score has specific permissions
|
154
|
+
#
|
155
|
+
# @param score [Float] The encoded score
|
156
|
+
# @param permissions [Array<Symbol>] Permissions to check
|
157
|
+
# @return [Boolean] True if all permissions are present
|
158
|
+
#
|
159
|
+
# @example
|
160
|
+
# permission?(1704067200.005, :read) # score has read(1) + write(4)
|
161
|
+
# #=> true
|
162
|
+
def permission?(score, *permissions)
|
163
|
+
decoded = decode_score(score)
|
164
|
+
permission_bits = decoded[:permissions]
|
165
|
+
|
166
|
+
permissions.all? do |perm|
|
167
|
+
flag = PERMISSION_FLAGS[perm]
|
168
|
+
flag && permission_bits.anybits?(flag)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Add permissions to existing score
|
173
|
+
#
|
174
|
+
# @param score [Float] The existing encoded score
|
175
|
+
# @param permissions [Array<Symbol>] Permissions to add
|
176
|
+
# @return [Float] New score with added permissions
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# add_permissions(1704067200.001, :write, :delete) # add write(4) + delete(32) to read(1)
|
180
|
+
# #=> 1704067200.037
|
181
|
+
def add_permissions(score, *permissions)
|
182
|
+
decoded = decode_score(score)
|
183
|
+
current_bits = decoded[:permissions]
|
184
|
+
|
185
|
+
new_bits = permissions.reduce(current_bits) do |acc, perm|
|
186
|
+
acc | (PERMISSION_FLAGS[perm] || 0)
|
187
|
+
end
|
188
|
+
|
189
|
+
encode_score(decoded[:timestamp], new_bits)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Remove permissions from existing score
|
193
|
+
#
|
194
|
+
# @param score [Float] The existing encoded score
|
195
|
+
# @param permissions [Array<Symbol>] Permissions to remove
|
196
|
+
# @return [Float] New score with removed permissions
|
197
|
+
#
|
198
|
+
# @example
|
199
|
+
# remove_permissions(1704067200.037, :write) # remove write(4) from read(1)+write(4)+delete(32)
|
200
|
+
# #=> 1704067200.033
|
201
|
+
def remove_permissions(score, *permissions)
|
202
|
+
decoded = decode_score(score)
|
203
|
+
current_bits = decoded[:permissions]
|
204
|
+
|
205
|
+
new_bits = permissions.reduce(current_bits) do |acc, perm|
|
206
|
+
acc & ~(PERMISSION_FLAGS[perm] || 0)
|
207
|
+
end
|
208
|
+
|
209
|
+
encode_score(decoded[:timestamp], new_bits)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Create score range for permissions
|
213
|
+
#
|
214
|
+
# @param min_permissions [Array<Symbol>, nil] Minimum required permissions
|
215
|
+
# @param max_permissions [Array<Symbol>, nil] Maximum allowed permissions
|
216
|
+
# @return [Array<Float>] Min and max scores for Redis range queries
|
217
|
+
#
|
218
|
+
# @example
|
219
|
+
# permission_range([:read], [:read, :write])
|
220
|
+
# #=> [0.001, 0.005]
|
221
|
+
def permission_range(min_permissions = [], max_permissions = nil)
|
222
|
+
min_bits = Array(min_permissions).reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) }
|
223
|
+
max_bits = if max_permissions
|
224
|
+
Array(max_permissions).reduce(0) do |acc, p|
|
225
|
+
acc | (PERMISSION_FLAGS[p] || 0)
|
226
|
+
end
|
227
|
+
else
|
228
|
+
255
|
229
|
+
end
|
230
|
+
|
231
|
+
[min_bits / METADATA_PRECISION, max_bits / METADATA_PRECISION]
|
232
|
+
end
|
233
|
+
|
234
|
+
# Get current timestamp as score (no permissions)
|
235
|
+
#
|
236
|
+
# @return [Float] Current time as Redis score
|
237
|
+
def current_score
|
238
|
+
encode_score(Time.now, 0)
|
239
|
+
end
|
240
|
+
|
241
|
+
# Create score range for Redis operations based on time bounds
|
242
|
+
#
|
243
|
+
# @param start_time [Time, nil] Start time (nil for -inf)
|
244
|
+
# @param end_time [Time, nil] End time (nil for +inf)
|
245
|
+
# @param min_permissions [Array<Symbol>, nil] Minimum required permissions
|
246
|
+
# @return [Array] Array suitable for Redis ZRANGEBYSCORE operations
|
247
|
+
#
|
248
|
+
# @example Time range
|
249
|
+
# score_range(1.hour.ago, Time.now)
|
250
|
+
# #=> [1704063600.0, 1704067200.255]
|
251
|
+
#
|
252
|
+
# @example Permission filter
|
253
|
+
# score_range(nil, nil, min_permissions: [:read])
|
254
|
+
# #=> [0.001, "+inf"]
|
255
|
+
def score_range(start_time = nil, end_time = nil, min_permissions: nil)
|
256
|
+
min_bits = if min_permissions
|
257
|
+
Array(min_permissions).reduce(0) do |acc, p|
|
258
|
+
acc | (PERMISSION_FLAGS[p] || 0)
|
259
|
+
end
|
260
|
+
else
|
261
|
+
0
|
262
|
+
end
|
263
|
+
|
264
|
+
min_score = if start_time
|
265
|
+
encode_score(start_time, min_bits)
|
266
|
+
elsif min_permissions
|
267
|
+
encode_score(0, min_bits)
|
268
|
+
else
|
269
|
+
'-inf'
|
270
|
+
end
|
271
|
+
|
272
|
+
max_score = if end_time
|
273
|
+
encode_score(end_time, 255) # Use max valid permission bits
|
274
|
+
else
|
275
|
+
'+inf'
|
276
|
+
end
|
277
|
+
|
278
|
+
[min_score, max_score]
|
279
|
+
end
|
280
|
+
|
281
|
+
# Decode permission bits into array of permission symbols
|
282
|
+
#
|
283
|
+
# @param bits [Integer] Permission bits to decode
|
284
|
+
# @return [Array<Symbol>] Array of permission symbols
|
285
|
+
def decode_permission_flags(bits)
|
286
|
+
PERMISSION_FLAGS.select { |_name, flag| bits.anybits?(flag) }.keys
|
287
|
+
end
|
288
|
+
|
289
|
+
# Check broad permission categories
|
290
|
+
#
|
291
|
+
# @param score [Float] The encoded score
|
292
|
+
# @param category [Symbol] Category to check (:readable, :content_editor, :administrator, etc.)
|
293
|
+
# @return [Boolean] True if score meets the category requirements
|
294
|
+
def category?(score, category)
|
295
|
+
decoded = decode_score(score)
|
296
|
+
permission_bits = decoded[:permissions]
|
297
|
+
|
298
|
+
mask = PERMISSION_CATEGORIES[category]
|
299
|
+
return false unless mask
|
300
|
+
|
301
|
+
permission_bits.anybits?(mask)
|
302
|
+
end
|
303
|
+
|
304
|
+
# Filter collection by permission category
|
305
|
+
#
|
306
|
+
# @param scores [Array<Float>] Array of scores to filter
|
307
|
+
# @param category [Symbol] Category to filter by
|
308
|
+
# @return [Array<Float>] Scores matching the category
|
309
|
+
def filter_by_category(scores, category)
|
310
|
+
mask = PERMISSION_CATEGORIES[category]
|
311
|
+
return [] unless mask
|
312
|
+
|
313
|
+
scores.select do |score|
|
314
|
+
permission_bits = ((score % 1) * METADATA_PRECISION).round
|
315
|
+
permission_bits.anybits?(mask)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Get permission tier for score
|
320
|
+
#
|
321
|
+
# @param score [Float] The encoded score
|
322
|
+
# @return [Symbol] Permission tier (:administrator, :content_editor, :viewer, :none)
|
323
|
+
def permission_tier(score)
|
324
|
+
decoded = decode_score(score)
|
325
|
+
bits = decoded[:permissions]
|
326
|
+
|
327
|
+
if bits.anybits?(PERMISSION_CATEGORIES[:administrator])
|
328
|
+
:administrator
|
329
|
+
elsif bits.anybits?(PERMISSION_CATEGORIES[:content_editor])
|
330
|
+
:content_editor
|
331
|
+
elsif bits.anybits?(PERMISSION_CATEGORIES[:readable])
|
332
|
+
:viewer
|
333
|
+
else
|
334
|
+
:none
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# Efficient bulk categorization
|
339
|
+
#
|
340
|
+
# @param scores [Array<Float>] Array of scores to categorize
|
341
|
+
# @return [Hash] Hash mapping tiers to arrays of scores
|
342
|
+
def categorize_scores(scores)
|
343
|
+
scores.group_by { |score| permission_tier(score) }
|
344
|
+
end
|
345
|
+
|
346
|
+
# Check if permissions meet minimum category
|
347
|
+
#
|
348
|
+
# @param permission_bits [Integer] Permission bits to check
|
349
|
+
# @param category [Symbol] Category to check against
|
350
|
+
# @return [Boolean] True if permissions meet the category requirements
|
351
|
+
def meets_category?(permission_bits, category)
|
352
|
+
mask = PERMISSION_CATEGORIES[category]
|
353
|
+
return false unless mask
|
354
|
+
|
355
|
+
case category
|
356
|
+
when :readable
|
357
|
+
permission_bits.positive? # Any permission implies read
|
358
|
+
when :privileged
|
359
|
+
permission_bits > 1 # More than just read
|
360
|
+
when :administrator
|
361
|
+
permission_bits.anybits?(PERMISSION_CATEGORIES[:administrator])
|
362
|
+
else
|
363
|
+
permission_bits.anybits?(mask)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
# Range queries for categorical filtering
|
368
|
+
#
|
369
|
+
# @param category [Symbol] Category to create range for
|
370
|
+
# @param start_time [Time, nil] Optional start time filter
|
371
|
+
# @param end_time [Time, nil] Optional end time filter
|
372
|
+
# @return [Array<String>] Min and max range strings for Redis queries
|
373
|
+
def category_score_range(category, start_time = nil, end_time = nil)
|
374
|
+
PERMISSION_CATEGORIES[category] || 0
|
375
|
+
|
376
|
+
# Any permission matching the category mask
|
377
|
+
min_score = start_time ? start_time.to_i : 0
|
378
|
+
max_score = end_time ? end_time.to_i : Time.now.to_i
|
379
|
+
|
380
|
+
# Return range that includes any matching permissions
|
381
|
+
["#{min_score}.000", "#{max_score}.999"]
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
# Validate permission bits are within acceptable range
|
387
|
+
#
|
388
|
+
# @param bits [Integer] Permission bits to validate
|
389
|
+
# @return [Integer] Validated permission bits
|
390
|
+
# @raise [ArgumentError] If bits are outside 0-255 range
|
391
|
+
def validate_permission_bits(bits)
|
392
|
+
raise ArgumentError, 'Permission bits must be 0-255' unless (0..255).cover?(bits)
|
393
|
+
|
394
|
+
bits
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Instance methods for classes that include this module
|
399
|
+
def encode_score(timestamp, permissions = 0)
|
400
|
+
ScoreEncoding.encode_score(timestamp, permissions)
|
401
|
+
end
|
402
|
+
|
403
|
+
def decode_score(score)
|
404
|
+
ScoreEncoding.decode_score(score)
|
405
|
+
end
|
406
|
+
|
407
|
+
def permission?(score, *permissions)
|
408
|
+
ScoreEncoding.permission?(score, *permissions)
|
409
|
+
end
|
410
|
+
|
411
|
+
def add_permissions(score, *permissions)
|
412
|
+
ScoreEncoding.add_permissions(score, *permissions)
|
413
|
+
end
|
414
|
+
|
415
|
+
def remove_permissions(score, *permissions)
|
416
|
+
ScoreEncoding.remove_permissions(score, *permissions)
|
417
|
+
end
|
418
|
+
|
419
|
+
def permission_range(min_permissions = [], max_permissions = nil)
|
420
|
+
ScoreEncoding.permission_range(min_permissions, max_permissions)
|
421
|
+
end
|
422
|
+
|
423
|
+
def current_score
|
424
|
+
ScoreEncoding.current_score
|
425
|
+
end
|
426
|
+
|
427
|
+
def score_range(start_time = nil, end_time = nil, min_permissions: nil)
|
428
|
+
ScoreEncoding.score_range(start_time, end_time, min_permissions: min_permissions)
|
429
|
+
end
|
430
|
+
|
431
|
+
# Legacy method aliases for backward compatibility
|
432
|
+
def permission_encode(timestamp, permission)
|
433
|
+
ScoreEncoding.permission_encode(timestamp, permission)
|
434
|
+
end
|
435
|
+
|
436
|
+
def permission_decode(score)
|
437
|
+
ScoreEncoding.permission_decode(score)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|