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

Instance Method Summary collapse

Class Method Details

.add_permissions(score, *permissions) ⇒ Float

Add permissions to existing score

Examples:

add_permissions(1704067200.001, :write, :delete)  # add write(4) + delete(32) to read(1)
#=> 1704067200.037

Parameters:

  • score (Float)

    The existing encoded score

  • permissions (Array<Symbol>)

    Permissions to add

Returns:

  • (Float)

    New score with added permissions



181
182
183
184
185
186
187
188
189
190
# File 'lib/familia/features/relationships/score_encoding.rb', line 181

def add_permissions(score, *permissions)
  decoded = decode_score(score)
  current_bits = decoded[:permissions]

  new_bits = permissions.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

Parameters:

  • scores (Array<Float>)

    Array of scores to categorize

Returns:

  • (Hash)

    Hash mapping tiers to arrays of scores



342
343
344
# File 'lib/familia/features/relationships/score_encoding.rb', line 342

def categorize_scores(scores)
  scores.group_by { |score| permission_tier(score) }
end

.category?(score, category) ⇒ Boolean

Check broad permission categories

Parameters:

  • score (Float)

    The encoded score

  • category (Symbol)

    Category to check (:readable, :content_editor, :administrator, etc.)

Returns:

  • (Boolean)

    True if score meets the category requirements



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)
  permission_bits = decoded[:permissions]

  mask = PERMISSION_CATEGORIES[category]
  return false unless mask

  permission_bits.anybits?(mask)
end

.category_score_range(category, start_time = nil, end_time = nil) ⇒ Array<String>

Range queries for categorical filtering

Parameters:

  • category (Symbol)

    Category to create range for

  • start_time (Time, nil) (defaults to: nil)

    Optional start time filter

  • end_time (Time, nil) (defaults to: nil)

    Optional end time filter

Returns:

  • (Array<String>)

    Min and max range strings for Redis queries



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 : Time.now.to_i

  # Return range that includes any matching permissions
  ["#{min_score}.000", "#{max_score}.999"]
end

.current_scoreFloat

Get current timestamp as score (no permissions)

Returns:

  • (Float)

    Current time as Redis score



237
238
239
# File 'lib/familia/features/relationships/score_encoding.rb', line 237

def current_score
  encode_score(Time.now, 0)
end

.decode_permission_flags(bits) ⇒ Array<Symbol>

Decode permission bits into array of permission symbols

Parameters:

  • bits (Integer)

    Permission bits to decode

Returns:

  • (Array<Symbol>)

    Array of permission symbols



285
286
287
# File 'lib/familia/features/relationships/score_encoding.rb', line 285

def decode_permission_flags(bits)
  PERMISSION_FLAGS.select { |_name, flag| bits.anybits?(flag) }.keys
end

.decode_score(score) ⇒ Hash

Decode a Redis score back into timestamp and permissions

Examples:

Basic decoding

decode_score(1704067200.037)
#=> { timestamp: 1704067200, permissions: 37, permission_list: [:read, :write, :delete] }

Parameters:

  • score (Float)

    The encoded score

Returns:

  • (Hash)

    Hash with :timestamp, :permissions, and :permission_list keys



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
  permission_bits = ((score - time_part) * METADATA_PRECISION).round

  {
    timestamp: time_part,
    permissions: permission_bits,
    permission_list: decode_permission_flags(permission_bits)
  }
end

.encode_score(timestamp, permissions = 0) ⇒ Float

Encode a timestamp and permissions into a Redis score

Examples:

Basic encoding with bit flag

encode_score(Time.now, 5)  # read(1) + write(4) = 5
#=> 1704067200.005

Permission symbol encoding

encode_score(Time.now, :read)
#=> 1704067200.001

Multiple permissions

encode_score(Time.now, [:read, :write, :delete])
#=> 1704067200.037

Parameters:

  • timestamp (Time, Integer)

    The timestamp to encode

  • permissions (Integer, Symbol, Array) (defaults to: 0)

    Permissions to encode

Returns:

  • (Float)

    Encoded score suitable for Redis sorted sets



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(timestamp, permissions = 0)
  time_part = timestamp.respond_to?(:to_i) ? timestamp.to_i : timestamp

  permission_bits = case permissions
                    when Symbol
                      PERMISSION_ROLES[permissions] || PERMISSION_FLAGS[permissions] || 0
                    when Array
                      # Support array of permission symbols
                      permissions.reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) }
                    when Integer
                      validate_permission_bits(permissions)
                    else
                      0
                    end

  time_part + (permission_bits / METADATA_PRECISION)
end

.filter_by_category(scores, category) ⇒ Array<Float>

Filter collection by permission category

Parameters:

  • scores (Array<Float>)

    Array of scores to filter

  • category (Symbol)

    Category to filter by

Returns:

  • (Array<Float>)

    Scores matching the 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|
    permission_bits = ((score % 1) * METADATA_PRECISION).round
    permission_bits.anybits?(mask)
  end
end

.meets_category?(permission_bits, category) ⇒ Boolean

Check if permissions meet minimum category

Parameters:

  • permission_bits (Integer)

    Permission bits to check

  • category (Symbol)

    Category to check against

Returns:

  • (Boolean)

    True if permissions meet the category requirements



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?(permission_bits, category)
  mask = PERMISSION_CATEGORIES[category]
  return false unless mask

  case category
  when :readable
    permission_bits.positive? # Any permission implies read
  when :privileged
    permission_bits > 1 # More than just read
  when :administrator
    permission_bits.anybits?(PERMISSION_CATEGORIES[:administrator])
  else
    permission_bits.anybits?(mask)
  end
end

.permission?(score, *permissions) ⇒ Boolean

Check if score has specific permissions

Examples:

permission?(1704067200.005, :read)  # score has read(1) + write(4)
#=> true

Parameters:

  • score (Float)

    The encoded score

  • permissions (Array<Symbol>)

    Permissions to check

Returns:

  • (Boolean)

    True if all permissions are present



162
163
164
165
166
167
168
169
170
# File 'lib/familia/features/relationships/score_encoding.rb', line 162

def permission?(score, *permissions)
  decoded = decode_score(score)
  permission_bits = decoded[:permissions]

  permissions.all? do |perm|
    flag = PERMISSION_FLAGS[perm]
    flag && permission_bits.anybits?(flag)
  end
end

.permission_decode(score) ⇒ Hash

Decode score into permission information

Parameters:

  • score (Float)

    The encoded score

Returns:

  • (Hash)

    Hash with timestamp, permissions bits, and permission list



87
88
89
90
91
92
93
94
# File 'lib/familia/features/relationships/score_encoding.rb', line 87

def permission_decode(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)

Parameters:

  • timestamp (Time, Integer)

    The timestamp to encode

  • permission (Symbol, Integer, Array)

    Permission(s) to encode

Returns:

  • (Float)

    Encoded score suitable for Redis sorted sets



79
80
81
# File 'lib/familia/features/relationships/score_encoding.rb', line 79

def permission_encode(timestamp, permission)
  encode_score(timestamp, permission)
end

.permission_level_value(permission) ⇒ Integer

Get permission bit flag value for a permission symbol

Parameters:

  • permission (Symbol)

    Permission symbol to get value for

Returns:

  • (Integer)

    Bit flag value for the permission

Raises:

  • (ArgumentError)

    If permission is unknown



70
71
72
# File 'lib/familia/features/relationships/score_encoding.rb', line 70

def permission_level_value(permission)
  PERMISSION_FLAGS[permission] || raise(ArgumentError, "Unknown permission: #{permission.inspect}")
end

.permission_range(min_permissions = [], max_permissions = nil) ⇒ Array<Float>

Create score range for permissions

Examples:

permission_range([:read], [:read, :write])
#=> [0.001, 0.005]

Parameters:

  • min_permissions (Array<Symbol>, nil) (defaults to: [])

    Minimum required permissions

  • max_permissions (Array<Symbol>, nil) (defaults to: nil)

    Maximum allowed permissions

Returns:

  • (Array<Float>)

    Min and max scores for Redis range queries



221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/familia/features/relationships/score_encoding.rb', line 221

def permission_range(min_permissions = [], max_permissions = nil)
  min_bits = Array(min_permissions).reduce(0) { |acc, p| acc | (PERMISSION_FLAGS[p] || 0) }
  max_bits = if max_permissions
               Array(max_permissions).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

Parameters:

  • score (Float)

    The encoded score

Returns:

  • (Symbol)

    Permission tier (:administrator, :content_editor, :viewer, :none)



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 permission_tier(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

Examples:

remove_permissions(1704067200.037, :write)  # remove write(4) from read(1)+write(4)+delete(32)
#=> 1704067200.033

Parameters:

  • score (Float)

    The existing encoded score

  • permissions (Array<Symbol>)

    Permissions to remove

Returns:

  • (Float)

    New score with removed permissions



201
202
203
204
205
206
207
208
209
210
# File 'lib/familia/features/relationships/score_encoding.rb', line 201

def remove_permissions(score, *permissions)
  decoded = decode_score(score)
  current_bits = decoded[:permissions]

  new_bits = permissions.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

Examples:

Time range

score_range(1.hour.ago, Time.now)
#=> [1704063600.0, 1704067200.255]

Permission filter

score_range(nil, nil, min_permissions: [:read])
#=> [0.001, "+inf"]

Parameters:

  • start_time (Time, nil) (defaults to: nil)

    Start time (nil for -inf)

  • end_time (Time, nil) (defaults to: nil)

    End time (nil for +inf)

  • min_permissions (Array<Symbol>, nil) (defaults to: nil)

    Minimum required permissions

Returns:

  • (Array)

    Array suitable for Redis ZRANGEBYSCORE operations



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 min_permissions
               Array(min_permissions).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 min_permissions
                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 add_permissions(score, *permissions)
  ScoreEncoding.add_permissions(score, *permissions)
end

#current_scoreObject



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(timestamp, permissions = 0)
  ScoreEncoding.encode_score(timestamp, permissions)
end

#permission?(score, *permissions) ⇒ Boolean

Returns:

  • (Boolean)


407
408
409
# File 'lib/familia/features/relationships/score_encoding.rb', line 407

def permission?(score, *permissions)
  ScoreEncoding.permission?(score, *permissions)
end

#permission_decode(score) ⇒ Object



436
437
438
# File 'lib/familia/features/relationships/score_encoding.rb', line 436

def permission_decode(score)
  ScoreEncoding.permission_decode(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 permission_encode(timestamp, permission)
  ScoreEncoding.permission_encode(timestamp, permission)
end

#permission_range(min_permissions = [], max_permissions = nil) ⇒ Object



419
420
421
# File 'lib/familia/features/relationships/score_encoding.rb', line 419

def permission_range(min_permissions = [], max_permissions = nil)
  ScoreEncoding.permission_range(min_permissions, max_permissions)
end

#remove_permissions(score, *permissions) ⇒ Object



415
416
417
# File 'lib/familia/features/relationships/score_encoding.rb', line 415

def remove_permissions(score, *permissions)
  ScoreEncoding.remove_permissions(score, *permissions)
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: min_permissions)
end