Module: Familia::Features::Relationships::ScoreEncoding
- Included in:
- ModelClassMethods
- 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 Valkey/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 Valkey/Redis score back into timestamp and permissions.
-
.encode_score(timestamp, permissions = 0) ⇒ Float
Encode a timestamp and permissions into a Valkey/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 db 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
181 182 183 184 185 186 187 188 189 190 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 181 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
342 343 344 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 342 def categorize_scores(scores) scores.group_by { |score| (score) } end |
.category?(score, category) ⇒ Boolean
Check broad permission categories
294 295 296 297 298 299 300 301 302 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 294 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
373 374 375 376 377 378 379 380 381 382 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 373 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 : Familia.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)
237 238 239 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 237 def current_score encode_score(Familia.now, 0) end |
.decode_permission_flags(bits) ⇒ Array<Symbol>
Decode permission bits into array of permission symbols
285 286 287 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 285 def (bits) PERMISSION_FLAGS.select { |_name, flag| bits.anybits?(flag) }.keys end |
.decode_score(score) ⇒ Hash
Decode a Valkey/Redis score back into timestamp and permissions
140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 140 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 Valkey/Redis score
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 114 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
309 310 311 312 313 314 315 316 317 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 309 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
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 351 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
162 163 164 165 166 167 168 169 170 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 162 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
88 89 90 91 92 93 94 95 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 88 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)
80 81 82 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 80 def (, ) encode_score(, ) end |
.permission_level_value(permission) ⇒ Integer
Get permission bit flag value for a permission symbol
71 72 73 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 71 def () PERMISSION_FLAGS[] || raise(ArgumentError, "Unknown permission: #{.inspect}") end |
.permission_range(min_permissions = [], max_permissions = nil) ⇒ Array<Float>
Create score range for permissions
221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 221 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
323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 323 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
201 202 203 204 205 206 207 208 209 210 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 201 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 db operations based on time bounds
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 255 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
411 412 413 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 411 def (score, *) ScoreEncoding.(score, *) end |
#current_score ⇒ Object
423 424 425 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 423 def current_score ScoreEncoding.current_score end |
#decode_score(score) ⇒ Object
403 404 405 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 403 def decode_score(score) ScoreEncoding.decode_score(score) end |
#encode_score(timestamp, permissions = 0) ⇒ Object
Instance methods for classes that include this module
399 400 401 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 399 def encode_score(, = 0) ScoreEncoding.encode_score(, ) end |
#permission?(score, *permissions) ⇒ Boolean
407 408 409 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 407 def (score, *) ScoreEncoding.(score, *) end |
#permission_decode(score) ⇒ Object
436 437 438 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 436 def (score) ScoreEncoding.(score) end |
#permission_encode(timestamp, permission) ⇒ Object
Legacy method aliases for backward compatibility
432 433 434 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 432 def (, ) ScoreEncoding.(, ) end |
#permission_range(min_permissions = [], max_permissions = nil) ⇒ Object
419 420 421 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 419 def ( = [], = nil) ScoreEncoding.(, ) end |
#remove_permissions(score, *permissions) ⇒ Object
415 416 417 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 415 def (score, *) ScoreEncoding.(score, *) end |
#score_range(start_time = nil, end_time = nil, min_permissions: nil) ⇒ Object
427 428 429 |
# File 'lib/familia/features/relationships/score_encoding.rb', line 427 def score_range(start_time = nil, end_time = nil, min_permissions: nil) ScoreEncoding.score_range(start_time, end_time, min_permissions: ) end |