redstruct 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|