Class: Redstruct::List

Inherits:
Struct show all
Includes:
Utils::Iterable, Utils::Scriptable
Defined in:
lib/redstruct/list.rb

Overview

Class to manipulate redis lists, modeled after Ruby’s Array class. TODO: Add maximum instance variable and modify all methods (where applicable) to take it into account.

Instance Attribute Summary

Attributes inherited from Struct

#key

Attributes inherited from Factory::Object

#factory

Instance Method Summary collapse

Methods included from Utils::Iterable

bound_enumerator, #each

Methods included from Utils::Scriptable

included

Methods inherited from Struct

#delete, #dump, #exists?, #expire, #expire_at, #initialize, #inspectable_attributes, #persist, #restore, #ttl, #type

Methods included from Utils::Coercion

coerce_array, coerce_bool

Methods inherited from Factory::Object

#connection, #initialize, #inspectable_attributes

Methods included from Utils::Inspectable

#inspect, #inspectable_attributes

Constructor Details

This class inherits a constructor from Redstruct::Struct

Instance Method Details

#<<(item) ⇒ Integer

Pushes the given element onto the list. As << is a binary operator, it can only take one argument in. It’s more of a convenience method.

Parameters:

  • item (#to_s)

    the item to append to the list

Returns:

  • (Integer)

    1 if appended, 0 otherwise



86
87
88
# File 'lib/redstruct/list.rb', line 86

def <<(item)
  return append(item)
end

#[](index) ⇒ String?

Returns the item located at index

Parameters:

  • index (Integer)

    the item located at index

Returns:

  • (String, nil)

    nil if no item at index, otherwise the value



31
32
33
# File 'lib/redstruct/list.rb', line 31

def [](index)
  return self.connection.lindex(@key, index.to_i)
end

#[]=(index, value) ⇒ Boolean

Sets or updates the value for item at index

Parameters:

  • index (Integer)

    the index

  • value (#to_s)

    the new value

Returns:

  • (Boolean)

    true if set, false otherwise

Raises:

  • Redis::BaseError when index is out of range



40
41
42
# File 'lib/redstruct/list.rb', line 40

def []=(index, value)
  return coerce_bool(set_script(keys: @key, argv: [index.to_i, value]))
end

#append(*items, max: 0, exists: false) ⇒ Integer Also known as: push

Appends the given items (from the right) to the list

Parameters:

  • items (Array<#to_s>)

    the items to append

  • max (Integer) (defaults to: 0)

    optional; if given, appends the items and trims down the list to max afterwards

  • exists (Boolean) (defaults to: false)

    optional; if true, only appends iff the list already exists (i.e. is not empty)

Returns:

  • (Integer)

    the number of items appended to the list



70
71
72
73
74
75
76
77
78
79
# File 'lib/redstruct/list.rb', line 70

def append(*items, max: 0, exists: false)
  max = max.to_i
  results = if max.positive? || exists
    push_and_trim_script(keys: @key, argv: [max - 1, false, exists] + items)
  else
    self.connection.rpush(@key, items)
  end

  return results
end

#clearObject

Clears the set by simply removing the key from the DB

See Also:

  • Struct#clear


17
18
19
# File 'lib/redstruct/list.rb', line 17

def clear
  delete
end

#empty?Boolean

Checks if the set is empty by checking if the key actually exists on the underlying redis db

Returns:

  • (Boolean)

    true if it is empty, false otherwise

See Also:



24
25
26
# File 'lib/redstruct/list.rb', line 24

def empty?
  return !exists?
end

#insert(value, index) ⇒ Object

Inserts the given value at the given zero-based index. TODO: Support multiple insertion like Array#insert? The biggest issue here is that concatenating lists in Lua is O(n), so on very large lists, this operation would become slow. There are Redis Modules which implement splice operations (so a O(1) list split/merge), but there’s no way to guarantee if the module will be present. Perhaps provide optional support if the module is detected?

Parameters:

  • value (#to_s)

    the value to insert

  • index (#to_i)

    the index at which to insert the value



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/redstruct/list.rb', line 53

def insert(value, index)
  result = case index
  when 0 then prepend(value)
  when -1 then append(value)
  else
    index += 1 if index.negative?
    insert_script(keys: @key, argv: [value, index])
  end

  return coerce_bool(result)
end

#pop(size = 1, timeout: nil) ⇒ nil, String

Pops an item from the list, optionally blocking to wait until the list is non-empty

Parameters:

  • timeout (Integer) (defaults to: nil)

    the amount of time to wait in seconds; if 0, waits indefinitely

Returns:

  • (nil, String)

    nil if the list was empty, otherwise the item

Raises:

  • (ArgumentError)


115
116
117
118
119
120
121
122
123
124
125
# File 'lib/redstruct/list.rb', line 115

def pop(size = 1, timeout: nil)
  raise ArgumentError, 'size must be positive' unless size.positive?

  if timeout.nil?
    return self.connection.rpop(@key) if size == 1
    return shift_pop_script(keys: @key, argv: [-size, -1, 1])
  else
    raise ArgumentError, 'timeout is only supported if size == 1' unless size == 1
    return self.connection.brpop(@key, timeout: timeout)&.last
  end
end

#popshift(list, timeout: nil) ⇒ String

Pops an element from this list and shifts it onto the given list.

Parameters:

  • list (Redstruct::List)

    the list to shift the element onto

  • timeout (#to_i) (defaults to: nil)

    optional timeout to wait for in seconds

Returns:

  • (String)

    the element that was popped from the list and pushed onto the other

Raises:

  • (ArgumentError)


146
147
148
149
150
151
152
153
154
# File 'lib/redstruct/list.rb', line 146

def popshift(list, timeout: nil)
  raise ArgumentError, 'list must respond to #key' unless list.respond_to?(:key)

  if timeout.nil?
    return self.connection.rpoplpush(@key, list.key)
  else
    return self.connection.brpoplpush(@key, list.key, timeout: timeout)
  end
end

#prepend(*items, max: nil, exists: false) ⇒ Integer Also known as: unshift

Prepends the given items (from the right) to the list

Parameters:

  • items (Array<#to_s>)

    the items to prepend

  • max (Integer) (defaults to: nil)

    optional; if given, prepends the items and trims down the list to max afterwards

  • exists (Boolean) (defaults to: false)

    optional; if true, only prepends iff the list already exists (i.e. is not empty)

Returns:

  • (Integer)

    the number of items prepended to the list



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/redstruct/list.rb', line 95

def prepend(*items, max: nil, exists: false)
  max = max.to_i

  # redis literally prepends each element one at a time, so 1 2 will end up 2 1
  # to keep behaviour in sync with Array#unshift we preemptively reverse the list
  items = items.reverse

  results = if max.positive? || exists
    push_and_trim_script(keys: @key, argv: [max - 1, true, exists] + items)
  else
    self.connection.lpush(@key, items)
  end

  return results
end

#remove(value, count: 1) ⇒ Integer

Removes the given item from the list.

Parameters:

  • count (Integer) (defaults to: 1)

    count > 0: Remove items equal to value moving from head to tail. count < 0: Remove items equal to value moving from tail to head. count = 0: Remove all items equal to value.

Returns:

  • (Integer)

    the number of items removed



161
162
163
# File 'lib/redstruct/list.rb', line 161

def remove(value, count: 1)
  self.connection.lrem(@key, count.to_i, value)
end

#shift(size = 1, timeout: nil) ⇒ nil, String

Shifts an item from the list, optionally blocking to wait until the list is non-empty

Parameters:

  • timeout (Integer) (defaults to: nil)

    the amount of time to wait in seconds; if 0, waits indefinitely

Returns:

  • (nil, String)

    nil if the list was empty, otherwise the item

Raises:

  • (ArgumentError)


130
131
132
133
134
135
136
137
138
139
140
# File 'lib/redstruct/list.rb', line 130

def shift(size = 1, timeout: nil)
  raise ArgumentError, 'size must be positive' unless size.positive?

  if timeout.nil?
    return self.connection.lpop(@key) if size == 1
    return shift_pop_script(keys: @key, argv: [0, size - 1, 0])
  else
    raise ArgumentError, 'timeout is only supported if size == 1' unless size == 1
    return self.connection.blpop(@key, timeout: timeout)&.last
  end
end

#sizeInteger

Checks how many items are in the list.

Returns:

  • (Integer)

    the number of items in the list



167
168
169
# File 'lib/redstruct/list.rb', line 167

def size
  return self.connection.llen(@key)
end

#slice(start: 0, length: -1)) ⇒ Array<String>

Returns a slice of the list starting at start (inclusively), up to length (inclusively)

Examples:

pry> list.slice(start: 1, length: 10) #=> Array<...> # array with 11 items

Parameters:

  • start (Integer) (defaults to: 0)

    the starting index for the slice; if start is larger than the end of the list, an empty list is returned

  • length (Integer) (defaults to: -1))

    the length of the slice (inclusively); if -1, returns everything

Returns:

  • (Array<String>)

    the requested slice, or an empty list



177
178
179
180
181
182
# File 'lib/redstruct/list.rb', line 177

def slice(start: 0, length: -1)
  length = length.to_i
  end_index = length.positive? ? start + length - 1 : length

  return self.connection.lrange(@key, start.to_i, end_index)
end

#to_aArray<String>

Loads all items into memory and returns an array. NOTE: if the list is expected to be large, use to_enum

Returns:

  • (Array<String>)

    the items in the list



187
188
189
# File 'lib/redstruct/list.rb', line 187

def to_a
  return slice(start: 0, length: -1)
end

#to_enum(match: '*', count: 10) ⇒ Enumerator

Since the list can be modified in between loops, this does not guarantee completion of the operation, nor that every single element of the list will be visited once; rather, it guarantees that it loops until no more elements are returned, using an incrementing offset. This means that should elements be removed in the meantime, they will not be seen, and others might be skipped as a result of this. If elements are added, it is however not an issue (although keep in mind that if elements are added faster than consumed, this can loop forever)

Returns:

  • (Enumerator)

    base enumerator to iterate of the list elements



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/redstruct/list.rb', line 200

def to_enum(match: '*', count: 10)
  pattern = Regexp.compile("^#{Regexp.escape(match).gsub('\*', '.*')}$")

  return Enumerator.new do |yielder|
    offset = 0
    loop do
      items = slice(start: offset, length: offset + count)

      offset += items.size
      matched = items.select { |item| item =~ pattern }
      yielder << matched unless matched.empty?

      raise StopIteration if items.size < count
    end
  end
end