Module: Familia::Features::Relationships::ScoreEncoding
- Included in:
- ClassMethods
- Defined in:
- lib/familia/features/relationships/score_encoding.rb
Overview
Score encoding using bit flags for permissions
Encodes permissions as bit flags in the decimal portion of Redis sorted set scores: - Integer part: Unix timestamp for time-based ordering - Decimal part: 8-bit permission flags (0-255)
Format: [timestamp].[permission_bits] Example: 1704067200.037 = Jan 1, 2024 with read(1) + write(4) + delete(32) = 37
Bit positions: 0: read - View/list items 1: append - Add new items 2: write - Modify existing items 3: edit - Edit metadata 4: configure - Change settings 5: delete - Remove items 6: transfer - Change ownership 7: admin - Full control
This allows combining permissions (read + delete without write) and efficient permission checking using bitwise operations while maintaining time-based ordering.
Constant Summary collapse
- MAX_METADATA =
Maximum value for metadata to preserve precision (3 decimal places) For 8-bit permission system, max value is 255
255- METADATA_PRECISION =
1000.0- PERMISSION_FLAGS =
Permission bit flags (8-bit system)
{ none: 0b00000000, # 0 - No permissions read: 0b00000001, # 1 - View/list append: 0b00000010, # 2 - Add new items write: 0b00000100, # 4 - Modify existing edit: 0b00001000, # 8 - Edit metadata configure: 0b00010000, # 16 - Change settings delete: 0b00100000, # 32 - Remove items transfer: 0b01000000, # 64 - Change ownership admin: 0b10000000, # 128 - Full control }.freeze
- PERMISSION_ROLES =
Predefined permission combinations
{ viewer: PERMISSION_FLAGS[:read], editor: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit], moderator: PERMISSION_FLAGS[:read] | PERMISSION_FLAGS[:write] | PERMISSION_FLAGS[:edit] | PERMISSION_FLAGS[:delete], admin: 0b11111111 # All permissions }.freeze
- PERMISSION_CATEGORIES =
Categorical masks for efficient broad queries
{ readable: 0b00000001, # Has basic access content_editor: 0b00001110, # Can modify content (append|write|edit) administrator: 0b11110000, # Has any admin powers privileged: 0b11111110, # Has beyond read-only owner: 0b11111111 # All permissions }.freeze
Class Method Summary collapse
-
.add_permissions(score, *permissions) ⇒ Float
Add permissions to existing score.
-
.categorize_scores(scores) ⇒ Hash
Efficient bulk categorization.
-
.category?(score, category) ⇒ Boolean
Check broad permission categories.
-
.category_score_range(category, start_time = nil, end_time = nil) ⇒ Array<String>
Range queries for categorical filtering.
-
.current_score ⇒ Float
Get current timestamp as score (no permissions).
-
.decode_permission_flags(bits) ⇒ Array<Symbol>
Decode permission bits into array of permission symbols.
-
.decode_score(score) ⇒ Hash
Decode a Redis score back into timestamp and permissions.
-
.encode_score(timestamp, permissions = 0) ⇒ Float
Encode a timestamp and permissions into a Redis score.
-
.filter_by_category(scores, category) ⇒ Array<Float>
Filter collection by permission category.
-
.meets_category?(permission_bits, category) ⇒ Boolean
Check if permissions meet minimum category.
-
.permission?(score, *permissions) ⇒ Boolean
Check if score has specific permissions.
-
.permission_decode(score) ⇒ Hash
Decode score into permission information.
-
.permission_encode(timestamp, permission) ⇒ Float
Encode timestamp and permission (alias for encode_score).
-
.permission_level_value(permission) ⇒ Integer
Get permission bit flag value for a permission symbol.
-
.permission_range(min_permissions = [], max_permissions = nil) ⇒ Array<Float>
Create score range for permissions.
-
.permission_tier(score) ⇒ Symbol
Get permission tier for score.
-
.remove_permissions(score, *permissions) ⇒ Float
Remove permissions from existing score.
-
.score_range(start_time = nil, end_time = nil, min_permissions: nil) ⇒ Array
Create score range for Redis operations based on time bounds.
Instance Method Summary collapse
-
#add_permissions(score, *permissions) ⇒ Object
-
#current_score ⇒ Object
-
#decode_score(score) ⇒ Object
-
#encode_score(timestamp, permissions = 0) ⇒ Object
Instance methods for classes that include this module.
-
#permission?(score, *permissions) ⇒ Boolean
-
#permission_decode(score) ⇒ Object
-
#permission_encode(timestamp, permission) ⇒ Object
Legacy method aliases for backward compatibility.
-
#permission_range(min_permissions = [], max_permissions = nil) ⇒ Object
-
#remove_permissions(score, *permissions) ⇒ Object
-
#score_range(start_time = nil, end_time = nil, min_permissions: nil) ⇒ Object
Class Method Details
.add_permissions(score, *permissions) ⇒ Float
Add permissions to existing score
179 180 181 182 183 184 185 186 187 188 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 179 def (score, *) decoded = decode_score(score) current_bits = decoded[:permissions] new_bits = .reduce(current_bits) do |acc, perm| acc | (PERMISSION_FLAGS[perm] || 0) end encode_score(decoded[:timestamp], new_bits) end |
.categorize_scores(scores) ⇒ Hash
Efficient bulk categorization
340 341 342 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 340 def categorize_scores(scores) scores.group_by { |score| (score) } end |
.category?(score, category) ⇒ Boolean
Check broad permission categories
292 293 294 295 296 297 298 299 300 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 292 def category?(score, category) decoded = decode_score(score) = decoded[:permissions] mask = PERMISSION_CATEGORIES[category] return false unless mask .anybits?(mask) end |
.category_score_range(category, start_time = nil, end_time = nil) ⇒ Array<String>
Range queries for categorical filtering
371 372 373 374 375 376 377 378 379 380 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 371 def category_score_range(category, start_time = nil, end_time = nil) PERMISSION_CATEGORIES[category] || 0 # Any permission matching the category mask min_score = start_time ? start_time.to_i : 0 max_score = end_time ? end_time.to_i : Time.now.to_i # Return range that includes any matching permissions ["#{min_score}.000", "#{max_score}.999"] end |
.current_score ⇒ Float
Get current timestamp as score (no permissions)
235 236 237 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 235 def current_score encode_score(Time.now, 0) end |
.decode_permission_flags(bits) ⇒ Array<Symbol>
Decode permission bits into array of permission symbols
283 284 285 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 283 def (bits) PERMISSION_FLAGS.select { |_name, flag| bits.anybits?(flag) }.keys end |
.decode_score(score) ⇒ Hash
Decode a Redis score back into timestamp and permissions
138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 138 def decode_score(score) return { timestamp: 0, permissions: 0, permission_list: [] } unless score.is_a?(Numeric) time_part = score.to_i = ((score - time_part) * METADATA_PRECISION).round { timestamp: time_part, permissions: , permission_list: () } end |
.encode_score(timestamp, permissions = 0) ⇒ Float
Encode a timestamp and permissions into a Redis score
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 112 def encode_score(, = 0) time_part = .respond_to?(:to_i) ? .to_i : = case when Symbol PERMISSION_ROLES[] || PERMISSION_FLAGS[] || 0 when Array # Support array of permission symbols .reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) } when Integer () else 0 end time_part + ( / METADATA_PRECISION) end |
.filter_by_category(scores, category) ⇒ Array<Float>
Filter collection by permission category
307 308 309 310 311 312 313 314 315 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 307 def filter_by_category(scores, category) mask = PERMISSION_CATEGORIES[category] return [] unless mask scores.select do |score| = ((score % 1) * METADATA_PRECISION).round .anybits?(mask) end end |
.meets_category?(permission_bits, category) ⇒ Boolean
Check if permissions meet minimum category
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 349 def meets_category?(, category) mask = PERMISSION_CATEGORIES[category] return false unless mask case category when :readable .positive? # Any permission implies read when :privileged > 1 # More than just read when :administrator .anybits?(PERMISSION_CATEGORIES[:administrator]) else .anybits?(mask) end end |
.permission?(score, *permissions) ⇒ Boolean
Check if score has specific permissions
160 161 162 163 164 165 166 167 168 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 160 def (score, *) decoded = decode_score(score) = decoded[:permissions] .all? do |perm| flag = PERMISSION_FLAGS[perm] flag && .anybits?(flag) end end |
.permission_decode(score) ⇒ Hash
Decode score into permission information
86 87 88 89 90 91 92 93 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 86 def (score) decoded = decode_score(score) { timestamp: decoded[:timestamp], permissions: decoded[:permissions], permission_list: decoded[:permission_list] } end |
.permission_encode(timestamp, permission) ⇒ Float
Encode timestamp and permission (alias for encode_score)
78 79 80 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 78 def (, ) encode_score(, ) end |
.permission_level_value(permission) ⇒ Integer
Get permission bit flag value for a permission symbol
69 70 71 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 69 def () PERMISSION_FLAGS[] || raise(ArgumentError, "Unknown permission: #{.inspect}") end |
.permission_range(min_permissions = [], max_permissions = nil) ⇒ Array<Float>
Create score range for permissions
219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 219 def ( = [], = nil) min_bits = Array().reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) } max_bits = if Array().reduce(0) do |acc, p| acc | (PERMISSION_FLAGS[p] || 0) end else 255 end [min_bits / METADATA_PRECISION, max_bits / METADATA_PRECISION] end |
.permission_tier(score) ⇒ Symbol
Get permission tier for score
321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 321 def (score) decoded = decode_score(score) bits = decoded[:permissions] if bits.anybits?(PERMISSION_CATEGORIES[:administrator]) :administrator elsif bits.anybits?(PERMISSION_CATEGORIES[:content_editor]) :content_editor elsif bits.anybits?(PERMISSION_CATEGORIES[:readable]) :viewer else :none end end |
.remove_permissions(score, *permissions) ⇒ Float
Remove permissions from existing score
199 200 201 202 203 204 205 206 207 208 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 199 def (score, *) decoded = decode_score(score) current_bits = decoded[:permissions] new_bits = .reduce(current_bits) do |acc, perm| acc & ~(PERMISSION_FLAGS[perm] || 0) end encode_score(decoded[:timestamp], new_bits) end |
.score_range(start_time = nil, end_time = nil, min_permissions: nil) ⇒ Array
Create score range for Redis operations based on time bounds
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 253 def score_range(start_time = nil, end_time = nil, min_permissions: nil) min_bits = if Array().reduce(0) do |acc, p| acc | (PERMISSION_FLAGS[p] || 0) end else 0 end min_score = if start_time encode_score(start_time, min_bits) elsif encode_score(0, min_bits) else '-inf' end max_score = if end_time encode_score(end_time, 255) # Use max valid permission bits else '+inf' end [min_score, max_score] end |
Instance Method Details
#add_permissions(score, *permissions) ⇒ Object
409 410 411 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 409 def (score, *) ScoreEncoding.(score, *) end |
#current_score ⇒ Object
421 422 423 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 421 def current_score ScoreEncoding.current_score end |
#decode_score(score) ⇒ Object
401 402 403 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 401 def decode_score(score) ScoreEncoding.decode_score(score) end |
#encode_score(timestamp, permissions = 0) ⇒ Object
Instance methods for classes that include this module
397 398 399 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 397 def encode_score(, = 0) ScoreEncoding.encode_score(, ) end |
#permission?(score, *permissions) ⇒ Boolean
405 406 407 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 405 def (score, *) ScoreEncoding.(score, *) end |
#permission_decode(score) ⇒ Object
434 435 436 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 434 def (score) ScoreEncoding.(score) end |
#permission_encode(timestamp, permission) ⇒ Object
Legacy method aliases for backward compatibility
430 431 432 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 430 def (, ) ScoreEncoding.(, ) end |
#permission_range(min_permissions = [], max_permissions = nil) ⇒ Object
417 418 419 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 417 def ( = [], = nil) ScoreEncoding.(, ) end |
#remove_permissions(score, *permissions) ⇒ Object
413 414 415 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 413 def (score, *) ScoreEncoding.(score, *) end |
#score_range(start_time = nil, end_time = nil, min_permissions: nil) ⇒ Object
425 426 427 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 425 def score_range(start_time = nil, end_time = nil, min_permissions: nil) ScoreEncoding.score_range(start_time, end_time, min_permissions: ) end |