redstruct 0.1.7 → 0.2.0
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/README.md +15 -11
- data/Rakefile +5 -5
- data/lib/redstruct/all.rb +14 -0
- data/lib/redstruct/configuration.rb +9 -6
- data/lib/redstruct/connection_proxy.rb +123 -0
- data/lib/redstruct/counter.rb +96 -0
- data/lib/redstruct/error.rb +2 -0
- data/lib/redstruct/factory/object.rb +31 -0
- data/lib/redstruct/factory.rb +94 -55
- data/lib/redstruct/hash.rb +123 -0
- data/lib/redstruct/list.rb +315 -0
- data/lib/redstruct/lock.rb +183 -0
- data/lib/redstruct/script.rb +104 -0
- data/lib/redstruct/set.rb +155 -0
- data/lib/redstruct/sorted_set/slice.rb +124 -0
- data/lib/redstruct/sorted_set.rb +153 -0
- data/lib/redstruct/string.rb +66 -0
- data/lib/redstruct/struct.rb +87 -0
- data/lib/redstruct/utils/coercion.rb +14 -8
- data/lib/redstruct/utils/inspectable.rb +8 -4
- data/lib/redstruct/utils/iterable.rb +52 -0
- data/lib/redstruct/utils/scriptable.rb +32 -6
- data/lib/redstruct/version.rb +4 -1
- data/lib/redstruct.rb +17 -51
- data/lib/yard/defscript_handler.rb +5 -3
- data/test/redstruct/configuration_test.rb +13 -0
- data/test/redstruct/connection_proxy_test.rb +85 -0
- data/test/redstruct/counter_test.rb +108 -0
- data/test/redstruct/factory/object_test.rb +21 -0
- data/test/redstruct/factory_test.rb +136 -0
- data/test/redstruct/hash_test.rb +138 -0
- data/test/redstruct/list_test.rb +244 -0
- data/test/redstruct/lock_test.rb +108 -0
- data/test/redstruct/script_test.rb +53 -0
- data/test/redstruct/set_test.rb +219 -0
- data/test/redstruct/sorted_set/slice_test.rb +10 -0
- data/test/redstruct/sorted_set_test.rb +219 -0
- data/test/redstruct/string_test.rb +8 -0
- data/test/redstruct/struct_test.rb +61 -0
- data/test/redstruct/utils/coercion_test.rb +33 -0
- data/test/redstruct/utils/inspectable_test.rb +31 -0
- data/test/redstruct/utils/iterable_test.rb +94 -0
- data/test/redstruct/utils/scriptable_test.rb +67 -0
- data/test/redstruct_test.rb +14 -0
- data/test/test_helper.rb +77 -1
- metadata +58 -26
- data/lib/redstruct/connection.rb +0 -47
- data/lib/redstruct/factory/creation.rb +0 -95
- data/lib/redstruct/factory/deserialization.rb +0 -7
- data/lib/redstruct/hls/lock.rb +0 -175
- data/lib/redstruct/hls/queue.rb +0 -29
- data/lib/redstruct/hls.rb +0 -2
- data/lib/redstruct/types/base.rb +0 -36
- data/lib/redstruct/types/counter.rb +0 -65
- data/lib/redstruct/types/hash.rb +0 -72
- data/lib/redstruct/types/list.rb +0 -76
- data/lib/redstruct/types/script.rb +0 -56
- data/lib/redstruct/types/set.rb +0 -96
- data/lib/redstruct/types/sorted_set.rb +0 -129
- data/lib/redstruct/types/string.rb +0 -64
- data/lib/redstruct/types/struct.rb +0 -58
- data/lib/releaser/logger.rb +0 -15
- data/lib/releaser/repository.rb +0 -32
- data/lib/tasks/release.rake +0 -49
- data/test/redstruct/restruct_test.rb +0 -4
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'redstruct/struct'
|
5
|
+
require 'redstruct/utils/iterable'
|
6
|
+
|
7
|
+
module Redstruct
|
8
|
+
# Mapping between Redis and Ruby sets. There is no caching mechanism in play, so most methods actually do access
|
9
|
+
# the underlying redis connection. Also, keep in mind Redis converts all values strings on the DB side
|
10
|
+
class Set < Redstruct::Struct
|
11
|
+
include Redstruct::Utils::Iterable
|
12
|
+
|
13
|
+
# Clears the set by simply removing the key from the DB
|
14
|
+
# @see Redstruct::Struct#delete
|
15
|
+
def clear
|
16
|
+
delete
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns random items from the set
|
20
|
+
# @param [Integer] count the number of items to return
|
21
|
+
# @return [String, Set] if count is one, then return the item; otherwise returns a set
|
22
|
+
def random(count: 1)
|
23
|
+
list = self.connection.srandmember(@key, count)
|
24
|
+
|
25
|
+
return nil if list.nil?
|
26
|
+
return count == 1 ? list[0] : ::Set.new(list)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Checks if the set is empty by checking if the key actually exists on the underlying redis db
|
30
|
+
# @see Redstruct::Struct#exists?
|
31
|
+
# @return [Boolean] true if it is empty, false otherwise
|
32
|
+
def empty?
|
33
|
+
return !exists?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks if the set contains this particular item
|
37
|
+
# @param [#to_s] item the item to check for
|
38
|
+
# @return [Boolean] true if the set contains the item, false otherwise
|
39
|
+
def contain?(item)
|
40
|
+
return coerce_bool(self.connection.sismember(@key, item))
|
41
|
+
end
|
42
|
+
alias include? contain?
|
43
|
+
|
44
|
+
# Adds the given items to the set
|
45
|
+
# @param [Array<#to_s>] items the items to add to the set
|
46
|
+
# @return [Boolean, Integer] when only one item, returns true or false on insertion, otherwise the number of items added
|
47
|
+
def add(*items)
|
48
|
+
return self.connection.sadd(@key, items)
|
49
|
+
end
|
50
|
+
alias << add
|
51
|
+
|
52
|
+
# Pops and returns an item from the set.
|
53
|
+
# NOTE: Since this is a redis set, keep in mind that popping the last element of the set effectively deletes the set
|
54
|
+
# @return [String] popped item
|
55
|
+
def pop
|
56
|
+
return self.connection.spop(@key)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Removes the given items from the set.
|
60
|
+
# @param [Array<#to_s>] items the items to remove from the set
|
61
|
+
# @return [Boolean, Integer] when only one item, returns true or false on deletion, otherwise the number of items removed
|
62
|
+
def remove(*items)
|
63
|
+
return self.connection.srem(@key, items)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Integer] the number of items in the set
|
67
|
+
def size
|
68
|
+
return self.connection.scard(@key).to_i
|
69
|
+
end
|
70
|
+
|
71
|
+
# Computes the difference of the two sets and stores the result in `dest`. If no destination provided, computes
|
72
|
+
# the results in memory.
|
73
|
+
# @param [Redstruct::Set] other set the set to subtract
|
74
|
+
# @param [Redstruct::Set, String] dest if nil, results are computed in memory. if a string, a new Redstruct::Set is
|
75
|
+
# constructed with the string as the key, and results are stored there. if already a Redstruct::Set, results are stored there.
|
76
|
+
# @return [::Set, Integer] if dest was provided, returns the number of elements in the destination; otherwise a standard Ruby set containing the difference
|
77
|
+
def difference(other, dest: nil)
|
78
|
+
destination = coerce_destination(dest)
|
79
|
+
results = if destination.nil?
|
80
|
+
::Set.new(self.connection.sdiff(@key, other.key))
|
81
|
+
else
|
82
|
+
self.connection.sdiffstore(destination.key, @key, other.key)
|
83
|
+
end
|
84
|
+
|
85
|
+
return results
|
86
|
+
end
|
87
|
+
alias - difference
|
88
|
+
|
89
|
+
# Computes the interesection of the two sets and stores the result in `dest`. If no destination provided, computes
|
90
|
+
# the results in memory.
|
91
|
+
# @param [Redstruct::Set] other set the set to intersect
|
92
|
+
# @param [Redstruct::Set, String] dest if nil, results are computed in memory. if a string, a new Redstruct::Set is
|
93
|
+
# constructed with the string as the key, and results are stored there. if already a Redstruct::Set, results are stored there.
|
94
|
+
# @return [::Set, Integer] if dest was provided, returns the number of elements in the destination; otherwise a standard Ruby set containing the intersection
|
95
|
+
def intersection(other, dest: nil)
|
96
|
+
destination = coerce_destination(dest)
|
97
|
+
results = if destination.nil?
|
98
|
+
::Set.new(self.connection.sinter(@key, other.key))
|
99
|
+
else
|
100
|
+
self.connection.sinterstore(destination.key, @key, other.key)
|
101
|
+
end
|
102
|
+
|
103
|
+
return results
|
104
|
+
end
|
105
|
+
alias | intersection
|
106
|
+
|
107
|
+
# Computes the union of the two sets and stores the result in `dest`. If no destination provided, computes
|
108
|
+
# the results in memory.
|
109
|
+
# @param [Redstruct::Set] other set the set to add
|
110
|
+
# @param [Redstruct::Set, String] dest if nil, results are computed in memory. if a string, a new Redstruct::Set is
|
111
|
+
# constructed with the string as the key, and results are stored there. if already a Redstruct::Set, results are stored there.
|
112
|
+
# @return [::Set, Integer] if dest was provided, returns the number of elements in the destination; otherwise a standard Ruby set containing the union
|
113
|
+
def union(other, dest: nil)
|
114
|
+
destination = coerce_destination(dest)
|
115
|
+
results = if destination.nil?
|
116
|
+
::Set.new(self.connection.sunion(@key, other.key))
|
117
|
+
else
|
118
|
+
self.connection.sunionstore(destination.key, @key, other.key)
|
119
|
+
end
|
120
|
+
|
121
|
+
return results
|
122
|
+
end
|
123
|
+
alias + union
|
124
|
+
|
125
|
+
# Use redis-rb sscan_each method to iterate over particular keys
|
126
|
+
# @return [Enumerator] base enumerator to iterate of the namespaced keys
|
127
|
+
def to_enum(match: '*', count: 10)
|
128
|
+
return self.connection.sscan_each(@key, match: match, count: count)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns an array representation of the set. Ordering is random and defined by redis
|
132
|
+
# NOTE: If the set is particularly large, consider using #each
|
133
|
+
# @return [Array<String>] an array of all items contained in the set
|
134
|
+
def to_a
|
135
|
+
return self.connection.smembers(@key)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Loads all members of the set and converts them to a Ruby set.
|
139
|
+
# NOTE: If the set is particularly large, consider using #each
|
140
|
+
# @return [::Set] ruby set of all items stored on redis for this set
|
141
|
+
def to_set
|
142
|
+
return ::Set.new(to_a)
|
143
|
+
end
|
144
|
+
|
145
|
+
def coerce_destination(dest)
|
146
|
+
case dest
|
147
|
+
when ::String
|
148
|
+
@factory.set(dest)
|
149
|
+
when self.class
|
150
|
+
dest
|
151
|
+
end
|
152
|
+
end
|
153
|
+
private :coerce_destination
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redstruct
|
4
|
+
class SortedSet
|
5
|
+
# Utility class to allow operations on portions of the set only
|
6
|
+
# TODO: Support #length property (using LIMIT offset count) of the different
|
7
|
+
# range commands, so a slice could be defined as starting at offset X and
|
8
|
+
# having length Y, instead of just starting at X and finishing at Y.
|
9
|
+
class Slice < Redstruct::Factory::Object
|
10
|
+
# @return [String] the key for the underlying sorted set
|
11
|
+
attr_reader :key
|
12
|
+
|
13
|
+
# @return [String, Float] the lower bound of the slice
|
14
|
+
attr_reader :lower
|
15
|
+
|
16
|
+
# @return [String, Float] the upper bound of the slice
|
17
|
+
attr_reader :upper
|
18
|
+
|
19
|
+
# @return [Boolean] if true, then assumes the slice is lexicographically sorted
|
20
|
+
attr_reader :lex
|
21
|
+
|
22
|
+
# @return [Boolean] if true, assumes the range bounds are exclusive
|
23
|
+
attr_reader :exclusive
|
24
|
+
|
25
|
+
# @param [String, Float] lower lower bound for the slice operation
|
26
|
+
# @param [String, Float] upper upper bound for the slice operation
|
27
|
+
# @param [Boolean] lex if true, uses lexicographic operations
|
28
|
+
# @param [Boolean] exclusive if true, assumes bounds are exclusive
|
29
|
+
def initialize(set, lower: nil, upper: nil, lex: false, exclusive: false)
|
30
|
+
super(factory: set.factory)
|
31
|
+
|
32
|
+
@key = set.key
|
33
|
+
@lex = lex
|
34
|
+
@exclusive = exclusive
|
35
|
+
|
36
|
+
lower ||= -Float::INFINITY
|
37
|
+
upper ||= Float::INFINITY
|
38
|
+
|
39
|
+
if @lex
|
40
|
+
@lower = parse_lex_bound(lower)
|
41
|
+
@upper = parse_lex_bound(upper)
|
42
|
+
else
|
43
|
+
@lower = parse_bound(lower)
|
44
|
+
@upper = parse_bound(upper)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Array<String>] returns an array of values for the given bounds
|
49
|
+
def to_a
|
50
|
+
if @lex
|
51
|
+
self.connection.zrangebylex(@key, @lower, @upper)
|
52
|
+
else
|
53
|
+
self.connection.zrangebyscore(@key, @lower, @upper)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array<String>] returns an array of values reversed
|
58
|
+
def reverse
|
59
|
+
if @lex
|
60
|
+
self.connection.zrevrangebylex(@key, @lower, @upper)
|
61
|
+
else
|
62
|
+
self.connection.zrevrangebyscore(@key, @lower, @upper)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Integer] the number of elements removed
|
67
|
+
def remove
|
68
|
+
if @lex
|
69
|
+
self.connection.zremrangebylex(@key, @lower, @upper)
|
70
|
+
else
|
71
|
+
self.connection.zremrangebyscore(@key, @lower, @upper)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Integer] number of elements in the slice
|
76
|
+
def size
|
77
|
+
if @lex
|
78
|
+
self.connection.zlexcount(@key, @lower, @upper)
|
79
|
+
else
|
80
|
+
self.connection.zcount(@key, @lower, @upper)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# TODO: consider using SortedSet, some other data structure, or nothing
|
85
|
+
# @return [::Set] an unordered set representation of the slice
|
86
|
+
def to_set
|
87
|
+
::Set.new(to_a)
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspectable_attributes
|
91
|
+
{ lower: @lower, upper: @upper, lex: @lex, exclusive: @exclusive, key: @key }
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# ( is exclusive, [ is inclusive
|
97
|
+
def parse_lex_bound(bound)
|
98
|
+
case bound
|
99
|
+
when -Float::INFINITY then '-'
|
100
|
+
when Float::INFINITY then '+'
|
101
|
+
else prefix(bound, inclusion: '[', exclusion: '(')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# ( is exclusive
|
106
|
+
def parse_bound(bound)
|
107
|
+
case bound
|
108
|
+
when -Float::INFINITY then '-inf'
|
109
|
+
when Float::INFINITY then '+inf'
|
110
|
+
when String then prefix(bound, exclusion: '(')
|
111
|
+
else prefix(bound.to_f, exclusion: '(')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def prefix(value, inclusion: '', exclusion: '')
|
116
|
+
prefix = @exclusive ? exclusion : inclusion
|
117
|
+
prefixed = value
|
118
|
+
prefixed = "#{prefix}#{value}" unless prefix.empty? || prefixed.to_s.start_with?(prefix)
|
119
|
+
|
120
|
+
return prefixed
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'redstruct/struct'
|
5
|
+
require 'redstruct/utils/iterable'
|
6
|
+
|
7
|
+
module Redstruct
|
8
|
+
# Mapping between Redis and Ruby sorted sets (with scores). There is no caching mechanism in play, so most methods actually do access
|
9
|
+
# the underlying redis connection. Also, keep in mind Redis converts all values strings on the DB side
|
10
|
+
class SortedSet < Redstruct::Struct
|
11
|
+
include Redstruct::Utils::Iterable
|
12
|
+
|
13
|
+
# @param [Boolean] lex if true, assumes the set is lexicographically sorted
|
14
|
+
def initialize(lex: false, **options)
|
15
|
+
super(**options)
|
16
|
+
@lex = lex
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Boolean] true if this is a lexicographically sorted set
|
20
|
+
def lexicographic?
|
21
|
+
return @lex
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [Array<#to_s>] values the object to add to the set
|
25
|
+
# @param [Boolean] exists if true, only update elements that exist (do not add new ones)
|
26
|
+
# @param [Boolean] overwrite if false, do not update existing elements
|
27
|
+
# @return [Integer] the number of elements that have changed (includes new ones)
|
28
|
+
def add(*values, exists: false, overwrite: true)
|
29
|
+
options = { xx: exists, nx: !overwrite, ch: true }
|
30
|
+
|
31
|
+
if @lex
|
32
|
+
values = values.map do |pair|
|
33
|
+
member = pair.is_a?(Array) ? pair.last : pair
|
34
|
+
[0.0, member]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
return self.connection.zadd(@key, values, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [#to_s] member the member of the set whose score to increment
|
42
|
+
# @param [#to_f] by the amount to increment the score by
|
43
|
+
# @return [Float] the new score of the member
|
44
|
+
def increment(member, by: 1.0)
|
45
|
+
raise NotImplementedError, 'cannot increment the score of items in a lexicographically ordered set' if @lex
|
46
|
+
return self.connection.zincrby(@key, by.to_f, member.to_s).to_f
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param [#to_s] member the member of the set whose score to decrement
|
50
|
+
# @param [#to_f] by the amount to decrement the score by
|
51
|
+
# @return [Float] the new score of the member
|
52
|
+
def decrement(member, by: 1.0)
|
53
|
+
return increment(member, by: -by.to_f)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Removes all items from the set. Does this by simply deleting the key
|
57
|
+
# @see Redstruct::Struct#delete
|
58
|
+
def clear
|
59
|
+
delete
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the number of items in the set. If you want to specify within a
|
63
|
+
# range, first get the slice and query its size.
|
64
|
+
# @return [Integer] the number of items in the set
|
65
|
+
def size
|
66
|
+
return self.connection.zcard(@key)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a slice or partial selection of the set.
|
70
|
+
# @see Redstruct::SortedSet::Slice#initialize
|
71
|
+
# @return [Redstruct::SortedSet::Slice] a newly created slice for this set
|
72
|
+
def slice(**options)
|
73
|
+
defaults = {
|
74
|
+
lower: nil,
|
75
|
+
upper: nil,
|
76
|
+
exclusive: false,
|
77
|
+
lex: @lex
|
78
|
+
}
|
79
|
+
|
80
|
+
self.class::Slice.new(self, **defaults.merge(options))
|
81
|
+
end
|
82
|
+
|
83
|
+
# Checks if the set contains any items.
|
84
|
+
# @return [Boolean] true if the key exists (meaning it contains at least 1 item), false otherwise
|
85
|
+
def empty?
|
86
|
+
return !exists?
|
87
|
+
end
|
88
|
+
|
89
|
+
# Relies on the score method, since it is O(1), whereas the index method is
|
90
|
+
# O(logn)
|
91
|
+
# @param [#to_s] item the item to check for
|
92
|
+
# @return [Boolean] true if the item is in the set, false otherwise
|
93
|
+
def contain?(item)
|
94
|
+
return coerce_bool(score(item))
|
95
|
+
end
|
96
|
+
alias include? contain?
|
97
|
+
|
98
|
+
# Returns the index of the item in the set, sorted ascending by score
|
99
|
+
# @param [#to_s] item the item to check for
|
100
|
+
# @return [Integer, nil] the index of the item, or nil if not found
|
101
|
+
def index(item)
|
102
|
+
return self.connection.zrank(@key, item)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns the index of the item in the set, sorted descending by score
|
106
|
+
# @param [#to_s] item the item to check for
|
107
|
+
# @return [Integer, nil] the index of the item, or nil if not found
|
108
|
+
def rindex(item)
|
109
|
+
return self.connection.zrevrank(@key, item)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns the score of the given item.
|
113
|
+
# @param [#to_s] item the item to check for
|
114
|
+
# @return [Float, nil] the score of the item, or nil if not found
|
115
|
+
def score(item)
|
116
|
+
return self.connection.zscore(@key, item)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Removes the items from the set.
|
120
|
+
# @param [Array<#to_s>] items the items to remove from the set
|
121
|
+
# @return [Integer] the amount of items removed from the set
|
122
|
+
def remove(*items)
|
123
|
+
return self.connection.zrem(@key, items)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns an array representation of the set, sorted by score ascending
|
127
|
+
# NOTE: It pulls the whole set into memory, so use each if that's a concern,
|
128
|
+
# or use slices with pre-determined ranges.
|
129
|
+
# @return [Array<Redstruct::Utils::ScoredValue>] all the items in the set, sorted by score ascending
|
130
|
+
def to_a
|
131
|
+
return slice.to_a
|
132
|
+
end
|
133
|
+
|
134
|
+
# TODO: Consider using ::SortedSet or some other data structure
|
135
|
+
# @return [::Set] an unordered set representation
|
136
|
+
def to_set
|
137
|
+
return slice.to_set
|
138
|
+
end
|
139
|
+
|
140
|
+
# Use redis-rb zscan_each method to iterate over particular keys
|
141
|
+
# @return [Enumerator] base enumerator to iterate of the namespaced keys
|
142
|
+
def to_enum(match: '*', count: 10, with_scores: false)
|
143
|
+
enumerator = self.connection.zscan_each(@key, match: match, count: count)
|
144
|
+
return enumerator if with_scores
|
145
|
+
return Enumerator.new do |yielder|
|
146
|
+
loop do
|
147
|
+
item, = enumerator.next
|
148
|
+
yielder << item
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redstruct/struct'
|
4
|
+
require 'redstruct/utils/scriptable'
|
5
|
+
|
6
|
+
module Redstruct
|
7
|
+
# Manipulation of redis strings
|
8
|
+
class String < Redstruct::Struct
|
9
|
+
include Redstruct::Utils::Scriptable
|
10
|
+
|
11
|
+
# @return [String, nil] the string value stored in the database
|
12
|
+
def get
|
13
|
+
return self.connection.get(@key)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [#to_s] value the object to store
|
17
|
+
# @param [Integer] expiry the expiry time in seconds; if nil, will never expire
|
18
|
+
# @param [Boolean] nx Not Exists: if true, will not set the key if it already existed
|
19
|
+
# @param [Boolean] xx Already Exists: if true, will set the key only if it already existed
|
20
|
+
# @return [Boolean] true if set, false otherwise
|
21
|
+
def set(value, expiry: nil, nx: false, xx: false)
|
22
|
+
options = { nx: nx, xx: xx }
|
23
|
+
options[:ex] = expiry.to_i unless expiry.nil?
|
24
|
+
|
25
|
+
coerce_bool(self.connection.set(@key, value, options))
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param [String] value The value to compare with
|
29
|
+
# @return [Boolean] True if deleted, false otherwise
|
30
|
+
def delete_if_equals(value)
|
31
|
+
coerce_bool(delete_if_equals_script(keys: @key, argv: value))
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [#to_s] value The object to store
|
35
|
+
# @return [String] The old value before setting it
|
36
|
+
def getset(value)
|
37
|
+
self.connection.getset(@key, value)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Integer] The length of the string
|
41
|
+
def length
|
42
|
+
self.connection.strlen(@key)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param [Integer] start Starting index of the slice
|
46
|
+
# @param [Integer] length Length of the slice; negative numbers start counting from the right (-1 = end)
|
47
|
+
# @return [Array<String>] The requested slice from <start> with length <length>
|
48
|
+
def slice(start = 0, length = -1)
|
49
|
+
length = start + length if length >= 0
|
50
|
+
return self.connection.getrange(@key, start, length)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Deletes the key (keys[1]) iff the value is equal to argv[1].
|
54
|
+
# @param [Array<(::String)>] keys The key to delete
|
55
|
+
# @param [Array<(::String)>] argv The value to compare with
|
56
|
+
# @return [Integer] 1 if deleted, 0 otherwise
|
57
|
+
defscript :delete_if_equals_script, <<~LUA
|
58
|
+
local deleted = false
|
59
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
60
|
+
deleted = redis.call("del", KEYS[1])
|
61
|
+
end
|
62
|
+
|
63
|
+
return deleted
|
64
|
+
LUA
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redstruct/factory/object'
|
4
|
+
require 'redstruct/utils/coercion'
|
5
|
+
|
6
|
+
module Redstruct
|
7
|
+
# Base class for all redis structures which have a particular value for a given key
|
8
|
+
class Struct < Redstruct::Factory::Object
|
9
|
+
include Redstruct::Utils::Coercion
|
10
|
+
|
11
|
+
# @return [String] the key used to identify the struct on redis
|
12
|
+
attr_reader :key
|
13
|
+
|
14
|
+
# @param [String] key the key used to identify the struct on redis, already namespaced
|
15
|
+
def initialize(key:, **options)
|
16
|
+
super(**options)
|
17
|
+
@key = key
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Boolean] Returns true if it exists in redis, false otherwise
|
21
|
+
def exists?
|
22
|
+
return self.connection.exists(@key)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Boolean] false if nothing was deleted in the DB, true if it was
|
26
|
+
def delete
|
27
|
+
return coerce_bool(self.connection.del(@key))
|
28
|
+
end
|
29
|
+
|
30
|
+
# Sets the key to expire after ttl seconds
|
31
|
+
# @param [#to_f] ttl the time to live in seconds (where 0.001 = 1ms)
|
32
|
+
# @return [Boolean] true if expired, false otherwise
|
33
|
+
def expire(ttl)
|
34
|
+
ttl = (ttl.to_f * 1000).floor
|
35
|
+
return coerce_bool(self.connection.pexpire(@key, ttl))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sets the key to expire at the given timestamp.
|
39
|
+
# @param [#to_f] time time or unix timestamp at which the key should expire; once converted to float, assumes 1.0 is one second, 0.001 is 1 ms
|
40
|
+
# @return [Boolean] true if expired, false otherwise
|
41
|
+
def expire_at(time)
|
42
|
+
time = (time.to_f * 1000).floor
|
43
|
+
return coerce_bool(self.connection.pexpireat(@key, time))
|
44
|
+
end
|
45
|
+
|
46
|
+
# Removes the expiry time from a key
|
47
|
+
# @return [Boolean] true if persisted, false otherwise
|
48
|
+
def persist
|
49
|
+
coerce_bool(self.connection.persist(@key))
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [String] the underlying redis type
|
53
|
+
def type
|
54
|
+
self.connection.type(@key)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the time to live of the key
|
58
|
+
# @return [Float] time to live in seconds as a float where 0.001 == 1 ms
|
59
|
+
def ttl
|
60
|
+
return self.connection.pttl(@key) / 1000.0
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a serialized representation of the key, which can be used to store a value externally, and restored to
|
64
|
+
# redis using #restore
|
65
|
+
# NOTE: This does not capture the TTL of the struct. If there arises a need for this, we can always modify it,
|
66
|
+
# but for now this is a pure proxy of the redis dump command
|
67
|
+
# @return [String, nil] nil if the struct does not exist, otherwise serialized representation
|
68
|
+
def dump
|
69
|
+
return self.connection.dump(@key)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Restores the struct to its serialized value as given
|
73
|
+
# @param [String] serialized serialized representation of the value
|
74
|
+
# @param [#to_f] ttl the time to live for the struct; defaults to 0 (meaning no expiry). 0.001 == 1ms
|
75
|
+
# @raise [Redis::CommandError] raised if the serialized value is incompatible or the key already exists
|
76
|
+
# @return [Boolean] true if restored, false otherwise
|
77
|
+
def restore(serialized, ttl: 0)
|
78
|
+
ttl = (ttl.to_f * 1000).floor
|
79
|
+
return self.connection.restore(@key, ttl, serialized)
|
80
|
+
end
|
81
|
+
|
82
|
+
# # @!visibility private
|
83
|
+
def inspectable_attributes
|
84
|
+
super.merge(key: @key)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Redstruct
|
2
4
|
module Utils
|
3
5
|
# Coercion utilities to map Redis replies to Ruby types, or vice-versa
|
@@ -9,10 +11,12 @@ module Redstruct
|
|
9
11
|
# @param [Object] value The value to coerce
|
10
12
|
# @return [Array] The coerced value
|
11
13
|
def coerce_array(value)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
case value
|
15
|
+
when nil then []
|
16
|
+
when Array then value
|
17
|
+
else
|
18
|
+
value.respond_to?(:to_a) ? value.to_a : [value]
|
19
|
+
end
|
16
20
|
end
|
17
21
|
module_function :coerce_array
|
18
22
|
|
@@ -22,10 +26,12 @@ module Redstruct
|
|
22
26
|
# @param [Object] value The object to coerce into a bool
|
23
27
|
# @return [Boolean] Coerced value
|
24
28
|
def coerce_bool(value)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
+
case value
|
30
|
+
when nil, false then false
|
31
|
+
when Numeric then !value.zero?
|
32
|
+
else
|
33
|
+
true
|
34
|
+
end
|
29
35
|
end
|
30
36
|
module_function :coerce_bool
|
31
37
|
end
|
@@ -1,6 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Redstruct
|
2
4
|
module Utils
|
5
|
+
# Adds helper methods for calling #inspect on a custom object
|
3
6
|
module Inspectable
|
7
|
+
# Generates a human readable list of attributes when inspecting a custom object
|
8
|
+
# @return [String]
|
4
9
|
def inspect
|
5
10
|
attributes = inspectable_attributes.map do |key, value|
|
6
11
|
"#{key}: <#{value.inspect}>"
|
@@ -8,14 +13,13 @@ module Redstruct
|
|
8
13
|
|
9
14
|
return "#{self.class.name}: #{attributes.join(', ')}"
|
10
15
|
end
|
16
|
+
alias to_s inspect
|
11
17
|
|
18
|
+
# To be overloaded by the including class
|
19
|
+
# @return [Hash<String, #inspect>] list of attributes that can be seen
|
12
20
|
def inspectable_attributes
|
13
21
|
{}
|
14
22
|
end
|
15
|
-
|
16
|
-
def to_s
|
17
|
-
return inspect
|
18
|
-
end
|
19
23
|
end
|
20
24
|
end
|
21
25
|
end
|