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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e699e92aa673d70280aa80258169eaad2c8edad
|
4
|
+
data.tar.gz: f1de7e921d57040ae2e74d1cc1db2b481b25d5f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d74321747fb7d5afa0d2f87ab1e7f6487b5e3a20289c071b5dd2a4ebb1068b601389de8d0e8003300436ae824331e6a5e9dc922e469503bdfb66ee6476270c37
|
7
|
+
data.tar.gz: 8b58b8d2c16aba6bc6efc9213b19d894fbad8a362a1466e372afa5fd448753f055219ef68596e18fc903eb929c579515bce05640c27bd50ff5d210227e78e8dd
|
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Redstruct
|
2
2
|
|
3
|
+
[](https://travis-ci.org/npepinpe/redstruct)
|
4
|
+
[](https://www.codacy.com/app/n.pepinpe/redstruct?utm_source=github.com&utm_medium=referral&utm_content=npepinpe/redstruct&utm_campaign=Badge_Grade)
|
5
|
+
|
3
6
|
Provides higher level data structures in Ruby using standard Redis commands. Also provides basic object mapping for pre-existing types.
|
4
7
|
|
5
8
|
## Installation
|
@@ -34,17 +37,18 @@ Avoid using transactions; the Redis documentation suggests using Lua scripts whe
|
|
34
37
|
|
35
38
|
## TODO
|
36
39
|
|
37
|
-
[x] Implement
|
38
|
-
[x] Implement
|
39
|
-
[x] Implement
|
40
|
-
[x] Implement
|
41
|
-
[
|
42
|
-
[
|
43
|
-
[
|
44
|
-
[
|
45
|
-
[
|
46
|
-
[ ]
|
47
|
-
[ ] Implement
|
40
|
+
- [x] Implement factory
|
41
|
+
- [x] Implement counter
|
42
|
+
- [x] Implement lock (blocking/non-blocking)
|
43
|
+
- [x] Implement string
|
44
|
+
- [x] Implement hash
|
45
|
+
- [x] Implement set
|
46
|
+
- [x] Implement list
|
47
|
+
- [x] Implement script
|
48
|
+
- [x] Implement sorted set
|
49
|
+
- [ ] Design/discuss stored factory meta-data (i.e. keep track of created objects, clear said objects, etc.)
|
50
|
+
- [ ] Implement collections to leverage redis commands such as mget, mset, etc.
|
51
|
+
- [ ] Implement value transformers (read and write) to make reusing types with specially encoded objects easy
|
48
52
|
|
49
53
|
|
50
54
|
## Contributing
|
data/Rakefile
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
2
|
require 'rake/testtask'
|
3
|
-
|
4
|
-
Bundler.require(:default, :development)
|
3
|
+
Bundler.require(:default, :rake)
|
5
4
|
|
6
5
|
Rake::TestTask.new(:test) do |t|
|
7
6
|
t.libs << 'test'
|
@@ -11,8 +10,9 @@ end
|
|
11
10
|
|
12
11
|
task default: :test
|
13
12
|
|
14
|
-
require 'yard/defscript_handler'
|
15
13
|
YARD::Rake::YardocTask.new do |t|
|
16
|
-
|
17
|
-
t.
|
14
|
+
require 'yard/defscript_handler'
|
15
|
+
t.files = ['lib/redstruct/*.rb']
|
16
|
+
t.options = ['--output-dir=./docs', '--no-private']
|
17
|
+
t.stats_options = ['--list-undoc']
|
18
18
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Convenience file to include the gem + all known structs
|
4
|
+
require 'redstruct'
|
5
|
+
|
6
|
+
# Structs; when creating a new one, add it here!
|
7
|
+
require 'redstruct/counter'
|
8
|
+
require 'redstruct/hash'
|
9
|
+
require 'redstruct/list'
|
10
|
+
require 'redstruct/lock'
|
11
|
+
require 'redstruct/set'
|
12
|
+
require 'redstruct/sorted_set'
|
13
|
+
require 'redstruct/sorted_set/slice'
|
14
|
+
require 'redstruct/string'
|
@@ -1,14 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Redstruct
|
4
|
+
# Simple class holding the Redstruct configuration
|
2
5
|
class Configuration
|
3
|
-
# @return [ConnectionPool]
|
4
|
-
attr_accessor :
|
6
|
+
# @return [ConnectionPool, Redis] the default redis-rb connection, or a pool of said connections
|
7
|
+
attr_accessor :default_connection
|
5
8
|
|
6
|
-
# @return [
|
7
|
-
attr_accessor :
|
9
|
+
# @return [String] Default namespace for factories
|
10
|
+
attr_accessor :default_namespace
|
8
11
|
|
9
12
|
def initialize
|
10
|
-
@
|
11
|
-
@
|
13
|
+
@default_connection = nil
|
14
|
+
@default_namespace = nil
|
12
15
|
end
|
13
16
|
end
|
14
17
|
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
require 'connection_pool'
|
5
|
+
require 'redstruct/utils/inspectable'
|
6
|
+
require 'redstruct/error'
|
7
|
+
|
8
|
+
module Redstruct
|
9
|
+
# Connection proxy class for the ConnectionPool
|
10
|
+
class ConnectionProxy
|
11
|
+
include Redstruct::Utils::Inspectable
|
12
|
+
|
13
|
+
# @return [Array<Symbol>] List of methods from the Redis class that we don't want to proxy
|
14
|
+
NON_COMMAND_METHODS = %i[[] []= _eval _scan method_missing call dup inspect to_s].freeze
|
15
|
+
|
16
|
+
# @param [Redis, ConnectionPool<Redis>] pool_or_conn a redis connection, or a pool of redis connections
|
17
|
+
# @raise [ArgumentError] raises an exception if the argument is not one of the required classes
|
18
|
+
def initialize(pool_or_conn)
|
19
|
+
case pool_or_conn
|
20
|
+
when ConnectionPool
|
21
|
+
@pool = pool_or_conn
|
22
|
+
when Redis
|
23
|
+
@redis = pool_or_conn
|
24
|
+
else
|
25
|
+
raise(ArgumentError, 'requires an instance of ConnectionPool or Redis to proxy to')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Executes the given block by first fixing a thread local connection from the pool,
|
30
|
+
# such that all redis commands executed within the block are on the same connection.
|
31
|
+
# This is necessary when doing pipelining, or multi/exec stuff
|
32
|
+
# @yield [Redis] a direct redis connection
|
33
|
+
# @return [Object] whatever the passed block evaluates to, nil otherwise
|
34
|
+
def with(&_block)
|
35
|
+
unless block_given?
|
36
|
+
Redstruct.logger.warn('do not Redstruct::ConnectionProxy#with with no block')
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
connection = @redis || Thread.current[:__redstruct_connection]
|
41
|
+
result = if connection.nil?
|
42
|
+
@pool.with do |c|
|
43
|
+
begin
|
44
|
+
Thread.current[:__redstruct_connection] = c
|
45
|
+
yield(c)
|
46
|
+
ensure
|
47
|
+
Thread.current[:__redstruct_connection] = nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
yield(connection)
|
52
|
+
end
|
53
|
+
|
54
|
+
return result
|
55
|
+
end
|
56
|
+
|
57
|
+
# While slower on load, defining all methods that we want to pipe to one of the connections results in
|
58
|
+
# faster calls at runtime, and gives us the convenience of not going through the pool.with everytime.
|
59
|
+
Redis.public_instance_methods(false).each do |method|
|
60
|
+
next if NON_COMMAND_METHODS.include?(method) || method_defined?(method)
|
61
|
+
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
62
|
+
# Proxy method for Redis##{method} to work with a connection pool
|
63
|
+
# Uses Redstruct::ConnectionProxy#with to obtain a connection (or the connection) and executes the method on it
|
64
|
+
def #{method}(*args, &block)
|
65
|
+
with { |c| c.#{method}(*args, &block) }
|
66
|
+
end
|
67
|
+
METHOD
|
68
|
+
end
|
69
|
+
|
70
|
+
# @!group redis-rb polyfills
|
71
|
+
# The following methods are methods not currently implemented in redis-rb,
|
72
|
+
# or only in trunk; they should be remove once support is wide-spread
|
73
|
+
|
74
|
+
# see: https://redis.io/commands/zlexcount
|
75
|
+
# zlexcount is not supported as of redis-rb 3.3.2
|
76
|
+
def zlexcount(key, min, max)
|
77
|
+
with do |c|
|
78
|
+
c.synchronize do |client|
|
79
|
+
client.call([:zlexcount, key, min, max])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @!endgroup
|
85
|
+
|
86
|
+
# Necessary when overwriting method_missing, so that respond_to? work properly
|
87
|
+
# @param [String, Symbol] _method the method name
|
88
|
+
# @param [Boolean] _include_private if true, also looks up private methods
|
89
|
+
# @return [Boolean] true if responding through method_missing, false otherwise
|
90
|
+
def respond_to_missing?(_method, _include_private = false)
|
91
|
+
true
|
92
|
+
end
|
93
|
+
private :respond_to_missing?
|
94
|
+
|
95
|
+
# Fallback when calling methods we may not have dynamically created above
|
96
|
+
# @param [String, Symbol] method the called method name
|
97
|
+
# @param [Array<Object>] args the arguments it was called with
|
98
|
+
# @param [Proc] block optionally, the block it was called with
|
99
|
+
# @return [Object] whatever the method returns
|
100
|
+
def method_missing(method, *args, &block)
|
101
|
+
with do |c|
|
102
|
+
if c.respond_to?(method)
|
103
|
+
c.public_send(method, *args, &block)
|
104
|
+
else
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# @!visibility private
|
111
|
+
def inspectable_attributes # :nodoc:
|
112
|
+
transport = if !@pool.nil?
|
113
|
+
'connection_pool'
|
114
|
+
elsif !@redis.nil?
|
115
|
+
'redis'
|
116
|
+
else
|
117
|
+
'nothing'
|
118
|
+
end
|
119
|
+
|
120
|
+
return { transport: transport }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redstruct/string'
|
4
|
+
|
5
|
+
module Redstruct
|
6
|
+
# Additional counter operations, using redis string values.
|
7
|
+
class Counter < Redstruct::String
|
8
|
+
# @return [Integer] the default increment value of the counter, defaults to 1
|
9
|
+
attr_reader :default_increment
|
10
|
+
|
11
|
+
# @return [Integer] the default maximum value of the counter, leave nil unless you want the cycle effect
|
12
|
+
attr_reader :max
|
13
|
+
|
14
|
+
# @param [Integer] by the default increment value
|
15
|
+
# @param [Integer, nil] max the default max value of the counter, leave nil unless you want cyclical counters
|
16
|
+
def initialize(by: 1, max: nil, **options)
|
17
|
+
super(**options)
|
18
|
+
@default_increment = by
|
19
|
+
@max = max
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Integer] the stored value as an integer
|
23
|
+
def get
|
24
|
+
return super.to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets the new value, converting it to int first
|
28
|
+
# @see Redstruct::String#set
|
29
|
+
# @param [#to_i] value the updated counter value
|
30
|
+
# @return [Boolean] True if set, false otherwise
|
31
|
+
def set(value, **options)
|
32
|
+
super(value.to_i, **options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [#to_i] value the object to store
|
36
|
+
# @return [Integer] the old value before setting it
|
37
|
+
def getset(value)
|
38
|
+
return super(value.to_i).to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
# Increments the counter by the given value. If max is given, will loop around and start again from 0 (will increment by (current_value + by) % max).
|
42
|
+
# @param [Integer, nil] by defaults to @increment, used to increment the underlying counter
|
43
|
+
# @param [Integer, nil] max if non-nil, the counter will loop and start over from 0 when it reaches max
|
44
|
+
# @example
|
45
|
+
# pry> counter.increment(by: 10, max: 5) # returns 0, since 10 % 5 == 0
|
46
|
+
# pry> counter.increment(by: 9, max: 5) # returns 4
|
47
|
+
# @return [Integer] the updated value
|
48
|
+
def increment(by: nil, max: nil)
|
49
|
+
by ||= @default_increment
|
50
|
+
max ||= @max
|
51
|
+
|
52
|
+
value = if max.nil?
|
53
|
+
self.connection.incrby(@key, by.to_i).to_i
|
54
|
+
else
|
55
|
+
ring_increment_script(keys: @key, argv: [by.to_i, max.to_i]).to_i
|
56
|
+
end
|
57
|
+
|
58
|
+
return value
|
59
|
+
end
|
60
|
+
|
61
|
+
# Decrements the counter by the given value. If max is given, will loop around and start again from 0 (will decrement by (current_value + by) % max).
|
62
|
+
# @param [Integer, nil] by defaults to @increment, used to increment the underlying counter
|
63
|
+
# @param [Integer, nil] max if non-nil, the counter will loop and start over from 0 when it reaches max
|
64
|
+
# @example
|
65
|
+
# pry> counter.decrement(by: 10, max: 5) # returns 0, since 10 % 5 == 0
|
66
|
+
# pry> counter.decrement(by: 9, max: 5) # returns -4
|
67
|
+
# @return [Integer] the updated value
|
68
|
+
def decrement(by: nil, max: nil)
|
69
|
+
by ||= @default_increment
|
70
|
+
by = -by.to_i
|
71
|
+
|
72
|
+
max ||= @max
|
73
|
+
max = -max unless max.nil?
|
74
|
+
|
75
|
+
return increment(by: by, max: max)
|
76
|
+
end
|
77
|
+
|
78
|
+
defscript :ring_increment_script, <<~LUA
|
79
|
+
local by = tonumber(ARGV[1])
|
80
|
+
local max = tonumber(ARGV[2])
|
81
|
+
local current = redis.call('get', KEYS[1])
|
82
|
+
local value = current and tonumber(current) or 0
|
83
|
+
|
84
|
+
value = (value + by) % max
|
85
|
+
redis.call('set', KEYS[1], value)
|
86
|
+
|
87
|
+
return value
|
88
|
+
LUA
|
89
|
+
protected :ring_increment_script
|
90
|
+
|
91
|
+
# @!visibility private
|
92
|
+
def inspectable_attributes
|
93
|
+
super.merge(max: @max, by: @default_increment)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/redstruct/error.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redstruct/utils/inspectable'
|
4
|
+
|
5
|
+
module Redstruct
|
6
|
+
class Factory
|
7
|
+
# Base class for all objects a factory can produce
|
8
|
+
class Object
|
9
|
+
include Redstruct::Utils::Inspectable
|
10
|
+
|
11
|
+
# @return [Redstruct::Factory] factory which was used to create the object
|
12
|
+
attr_reader :factory
|
13
|
+
|
14
|
+
# @param [Redstruct::Factory] factory the factory which produced the object
|
15
|
+
def initialize(factory:)
|
16
|
+
@factory = factory
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convenience accessor for the factory's connection
|
20
|
+
# @return [Redstruct::ConnectionProxy]
|
21
|
+
def connection
|
22
|
+
return @factory.connection
|
23
|
+
end
|
24
|
+
|
25
|
+
# # @!visibility private
|
26
|
+
def inspectable_attributes
|
27
|
+
{ factory: @factory }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/redstruct/factory.rb
CHANGED
@@ -1,85 +1,124 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redstruct/error'
|
4
|
+
require 'redstruct/connection_proxy'
|
5
|
+
require 'redstruct/utils/inspectable'
|
6
|
+
require 'redstruct/utils/iterable'
|
7
|
+
|
8
|
+
# Default objects; the rest you have to require
|
9
|
+
require 'redstruct/factory/object'
|
10
|
+
require 'redstruct/struct'
|
11
|
+
require 'redstruct/script'
|
12
|
+
|
2
13
|
module Redstruct
|
3
14
|
# Main interface of the gem; this class should be used to build all Redstruct
|
4
15
|
# objects, even when deserializing them.
|
5
16
|
class Factory
|
6
|
-
include Redstruct::Utils::Inspectable
|
7
|
-
|
17
|
+
include Redstruct::Utils::Inspectable
|
18
|
+
include Redstruct::Utils::Iterable
|
19
|
+
|
20
|
+
# @return [String] namespace used to prefix the keys of all objects created by this factory
|
21
|
+
attr_reader :namespace
|
8
22
|
|
9
|
-
# @return [
|
23
|
+
# @return [Redstruct::ConnectionProxy] connection proxy used to execute commands
|
10
24
|
attr_reader :connection
|
11
25
|
|
12
|
-
# @param [Redstruct::
|
13
|
-
# @param [
|
14
|
-
# @
|
15
|
-
# @return [Factory]
|
16
|
-
def initialize(connection: nil,
|
17
|
-
namespace ||= Redstruct.config.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
raise(Redstruct::Error, 'A connection pool is required to create a factory, but none was given') if pool.nil?
|
22
|
-
connection = Redstruct::Connection.new(pool)
|
23
|
-
end
|
26
|
+
# @param [Redstruct::ConnectionProxy] connection connection to use for all objects built by the factory
|
27
|
+
# @param [String] namespace optional; all objects built from the factory will have their keys prefixed with this
|
28
|
+
# @raise [ArgumentError] raised if connection is not nil, and not a Redstruct::ConnectionProxy
|
29
|
+
# @return [Redstruct::Factory]
|
30
|
+
def initialize(connection: nil, namespace: nil)
|
31
|
+
namespace ||= Redstruct.config.default_namespace
|
32
|
+
connection ||= Redstruct::ConnectionProxy.new(Redstruct.config.default_connection)
|
33
|
+
|
34
|
+
raise ArgumentError, 'connection should be a Redstruct::ConnectionProxy' unless connection.is_a?(Redstruct::ConnectionProxy)
|
24
35
|
|
25
36
|
@connection = connection
|
26
|
-
@namespace = namespace
|
27
|
-
@script_cache = {}.tap { |hash| hash.extend(MonitorMixin) }
|
37
|
+
@namespace = namespace.to_s
|
28
38
|
end
|
29
39
|
|
30
40
|
# Returns a namespaced version of the key (unless already namespaced)
|
31
|
-
# @param [String] key the key to
|
41
|
+
# @param [String] key the key to namespace
|
32
42
|
# @return [String] namespaced version of the key (or the key itself if already namespaced)
|
33
|
-
def
|
34
|
-
|
35
|
-
|
43
|
+
def prefix(key)
|
44
|
+
prefixed = key
|
45
|
+
prefixed = "#{@namespace}:#{key}" unless @namespace.empty? || key.start_with?("#{@namespace}:")
|
36
46
|
|
37
|
-
|
38
|
-
|
39
|
-
# @param [String] match will prepend the factory namespace to the match string; see the redis documentation for the syntax
|
40
|
-
# @param [Integer] count maximum number of items returned per iteration
|
41
|
-
# @param [Integer] max maximum number of iterations; if none given, could potentially never terminate
|
42
|
-
# @return [Enumerator::Lazy] if no block given, returns an enumerator that you can chain with others
|
43
|
-
def each(match: '*', count: nil, max: 10_000, &block)
|
44
|
-
options = { match: isolate(match) }
|
45
|
-
options[:count] = count.to_i unless count.nil?
|
46
|
-
|
47
|
-
enumerator = @connection.scan_each(options)
|
48
|
-
enumerator = enumerator.each_slice(count) unless count.nil?
|
49
|
-
|
50
|
-
# creates a temporary enumerator which limits the number of possible iterations, ensuring this eventually finishes
|
51
|
-
unless max.nil?
|
52
|
-
unbounded_enumerator = enumerator
|
53
|
-
enumerator = Enumerator.new do |yielder|
|
54
|
-
iterations = 0
|
55
|
-
loop do
|
56
|
-
yielder << unbounded_enumerator.next
|
57
|
-
iterations += 1
|
58
|
-
raise StopIteration if iterations == max
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
47
|
+
return prefixed
|
48
|
+
end
|
62
49
|
|
63
|
-
|
64
|
-
|
50
|
+
# Use redis-rb scan_each method to iterate over particular keys
|
51
|
+
# @
|
52
|
+
# @return [Enumerator] base enumerator to iterate of the namespaced keys
|
53
|
+
def to_enum(match: '*', count: 10)
|
54
|
+
return @connection.scan_each(match: prefix(match), count: count)
|
65
55
|
end
|
66
56
|
|
67
57
|
# Deletes all keys created by the factory. By defaults will iterate at most of 500 million keys
|
68
58
|
# @param [Hash] options accepts the options as given in each
|
69
59
|
# @see Redstruct::Factory#each
|
70
|
-
def
|
71
|
-
return each({ match: '*', count: 500,
|
60
|
+
def delete(options = {})
|
61
|
+
return each({ match: '*', count: 500, max_iterations: 1_000_000, batch_size: 500 }.merge(options)) do |keys|
|
72
62
|
@connection.del(*keys)
|
73
63
|
end
|
74
64
|
end
|
75
65
|
|
76
|
-
#
|
66
|
+
# @!group Factory methods
|
77
67
|
|
78
|
-
#
|
79
|
-
|
80
|
-
|
68
|
+
# Returns a factory for a sub namespace.
|
69
|
+
# @example Given a factory `f` with namespace fact:first
|
70
|
+
# f.factory('second') # => Redstruct::Factory: namespace: <"fact:first:second">>
|
71
|
+
# @return [Factory] namespaced factory
|
72
|
+
def factory(namespace)
|
73
|
+
return self.class.new(connection: @connection, namespace: prefix(namespace))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates using this factory's connection
|
77
|
+
# @see Redstruct::Script#new
|
78
|
+
# @return [Redstruct::Script] script sharing the factory connection
|
79
|
+
def script(script, **options)
|
80
|
+
return Redstruct::Script.new(script: script, connection: @connection, **options)
|
81
81
|
end
|
82
82
|
|
83
|
-
#
|
83
|
+
# Creates a lock for the given resource within this factory
|
84
|
+
# @see Redstruct::Lock#new
|
85
|
+
# @return [Redstruct::Lock] lock for the given resource within this factory
|
86
|
+
def lock(resource, **options)
|
87
|
+
return Redstruct::Lock.new(resource, factory: self, **options)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Factory methods for struct classes
|
91
|
+
{
|
92
|
+
Counter: :counter,
|
93
|
+
Hash: :hashmap,
|
94
|
+
List: :list,
|
95
|
+
Set: :set,
|
96
|
+
SortedSet: :sorted_set,
|
97
|
+
String: :string,
|
98
|
+
Struct: :struct
|
99
|
+
}.each do |class_name, method_name|
|
100
|
+
unless defined?(class_name)
|
101
|
+
Redstruct.logger.warn("cannot define factory method #{method_name} for non-existing class Redstruct::#{class_name}")
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
if method_defined?(method_name)
|
106
|
+
Redstruct.logger.warn("trying to redefine Redstruct::Factory##{method_name}; already defined?")
|
107
|
+
next
|
108
|
+
end
|
109
|
+
|
110
|
+
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
111
|
+
def #{method_name}(key, **options)
|
112
|
+
return Redstruct::#{class_name}.new(key: prefix(key), factory: self, **options)
|
113
|
+
end
|
114
|
+
METHOD
|
115
|
+
end
|
116
|
+
|
117
|
+
# @!endgroup
|
118
|
+
|
119
|
+
# # @!visibility private
|
120
|
+
def inspectable_attributes
|
121
|
+
return { namespace: @namespace, connection: @connection }
|
122
|
+
end
|
84
123
|
end
|
85
124
|
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redstruct/struct'
|
4
|
+
require 'redstruct/utils/iterable'
|
5
|
+
|
6
|
+
module Redstruct
|
7
|
+
# Class to manipulate redis hashes, modeled after Ruby's Hash class.
|
8
|
+
class Hash < Redstruct::Struct
|
9
|
+
include Redstruct::Utils::Iterable
|
10
|
+
|
11
|
+
# Returns the value at key
|
12
|
+
# @param [#to_s] key the hash key
|
13
|
+
# @return [nil, String] the value at key, or nil if nothing
|
14
|
+
def [](key)
|
15
|
+
return self.connection.hget(@key, key)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the value at key
|
19
|
+
# @param [Array<#to_s>] keys a list of keys to fetch; can be only one
|
20
|
+
# @return [Hash<String, String>] if only one key was passed, then return the value for it; otherwise returns a Ruby hash
|
21
|
+
# where each key in the `keys` is mapped to the value returned by redis
|
22
|
+
def get(*keys)
|
23
|
+
return self.connection.hget(@key, keys.first) if keys.size == 1
|
24
|
+
return self.connection.mapped_hmget(@key, *keys).reject { |_, v| v.nil? }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets or updates the value at key
|
28
|
+
# @param [#to_s] key the hash key
|
29
|
+
# @param [#to_s] value the new value to set
|
30
|
+
# @return [Boolean] true if the field was set (not updated!), false otherwise
|
31
|
+
def []=(key, value)
|
32
|
+
set(key, value, overwrite: true)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sets or updates the value at key
|
36
|
+
# @param [#to_s] key the hash key
|
37
|
+
# @param [#to_s] value the new value to set
|
38
|
+
# @return [Boolean] true if the field was set (not updated!), false otherwise
|
39
|
+
def set(key, value, overwrite: true)
|
40
|
+
result = if overwrite
|
41
|
+
self.connection.hset(@key, key, value)
|
42
|
+
else
|
43
|
+
self.connection.hsetnx(@key, key, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
return coerce_bool(result)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Updates the underlying redis hash using the given Ruby hash's key/value mapping
|
50
|
+
# @param [Hash] hash the key/value mapping to use
|
51
|
+
# @return [Boolean] true if updated, false otherwise
|
52
|
+
def update(hash)
|
53
|
+
coerce_bool(self.connection.mapped_hmset(@key, hash))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Removes all items for the given keys
|
57
|
+
# @param [Array<#to_s>] keys the list of keys to remove
|
58
|
+
# @return [Integer] the number of keys removed
|
59
|
+
def remove(*keys)
|
60
|
+
return self.connection.hdel(@key, keys)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Checks if a key has a value
|
64
|
+
# @param [#to_s] key the key to check for
|
65
|
+
# @return [Boolean] true if the key has a value associated, false otherwise
|
66
|
+
def key?(key)
|
67
|
+
return coerce_bool(self.connection.hexists(@key, key))
|
68
|
+
end
|
69
|
+
|
70
|
+
# Increments the value at the given key
|
71
|
+
# @param [#to_s] key the hash key
|
72
|
+
# @param [Integer, Float] by defaults to 1
|
73
|
+
# @return [Integer, Float] returns the incremented value
|
74
|
+
def increment(key, by: 1)
|
75
|
+
if by.is_a?(Float)
|
76
|
+
self.connection.hincrbyfloat(@key, key, by.to_f).to_f
|
77
|
+
else
|
78
|
+
self.connection.hincrby(@key, key, by.to_i).to_i
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Decrements the value at the given key
|
83
|
+
# @param [#to_s] key the hash key
|
84
|
+
# @param [Integer, Float] by defaults to 1
|
85
|
+
# @return [Integer, Float] returns the decremented value
|
86
|
+
def decrement(key, by: 1)
|
87
|
+
return increment(key, by: -by)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Boolean] true if the hash contains no elements
|
91
|
+
def empty?
|
92
|
+
return !exists?
|
93
|
+
end
|
94
|
+
|
95
|
+
# Loads all the hash members in memory
|
96
|
+
# NOTE: if the hash is expected to be large, use to_enum
|
97
|
+
# @return [Hash<String, String>] all key value pairs stored on redis
|
98
|
+
def to_h
|
99
|
+
return self.connection.hgetall(@key)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Array<String>] a list of all hash keys with values associated
|
103
|
+
def keys
|
104
|
+
return self.connection.hkeys(@key)
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [Array<Strign>] a list of all hash values
|
108
|
+
def values
|
109
|
+
return self.connection.hvals(@key)
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [Integer] the number of key value pairs stored for this hash
|
113
|
+
def size
|
114
|
+
return self.connection.hlen(@key)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Use redis-rb hscan_each method to iterate over particular keys
|
118
|
+
# @return [Enumerator] base enumerator to iterate of the namespaced keys
|
119
|
+
def to_enum(match: '*', count: 10)
|
120
|
+
return self.connection.hscan_each(@key, match: match, count: count)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|