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
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
|
+
[![Build Status](https://travis-ci.org/npepinpe/redstruct.svg?branch=master)](https://travis-ci.org/npepinpe/redstruct)
|
4
|
+
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/a97a31e37fa4432d8836169fa1999d34)](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
|