redstruct 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -11
  3. data/Rakefile +5 -5
  4. data/lib/redstruct/all.rb +14 -0
  5. data/lib/redstruct/configuration.rb +9 -6
  6. data/lib/redstruct/connection_proxy.rb +123 -0
  7. data/lib/redstruct/counter.rb +96 -0
  8. data/lib/redstruct/error.rb +2 -0
  9. data/lib/redstruct/factory/object.rb +31 -0
  10. data/lib/redstruct/factory.rb +94 -55
  11. data/lib/redstruct/hash.rb +123 -0
  12. data/lib/redstruct/list.rb +315 -0
  13. data/lib/redstruct/lock.rb +183 -0
  14. data/lib/redstruct/script.rb +104 -0
  15. data/lib/redstruct/set.rb +155 -0
  16. data/lib/redstruct/sorted_set/slice.rb +124 -0
  17. data/lib/redstruct/sorted_set.rb +153 -0
  18. data/lib/redstruct/string.rb +66 -0
  19. data/lib/redstruct/struct.rb +87 -0
  20. data/lib/redstruct/utils/coercion.rb +14 -8
  21. data/lib/redstruct/utils/inspectable.rb +8 -4
  22. data/lib/redstruct/utils/iterable.rb +52 -0
  23. data/lib/redstruct/utils/scriptable.rb +32 -6
  24. data/lib/redstruct/version.rb +4 -1
  25. data/lib/redstruct.rb +17 -51
  26. data/lib/yard/defscript_handler.rb +5 -3
  27. data/test/redstruct/configuration_test.rb +13 -0
  28. data/test/redstruct/connection_proxy_test.rb +85 -0
  29. data/test/redstruct/counter_test.rb +108 -0
  30. data/test/redstruct/factory/object_test.rb +21 -0
  31. data/test/redstruct/factory_test.rb +136 -0
  32. data/test/redstruct/hash_test.rb +138 -0
  33. data/test/redstruct/list_test.rb +244 -0
  34. data/test/redstruct/lock_test.rb +108 -0
  35. data/test/redstruct/script_test.rb +53 -0
  36. data/test/redstruct/set_test.rb +219 -0
  37. data/test/redstruct/sorted_set/slice_test.rb +10 -0
  38. data/test/redstruct/sorted_set_test.rb +219 -0
  39. data/test/redstruct/string_test.rb +8 -0
  40. data/test/redstruct/struct_test.rb +61 -0
  41. data/test/redstruct/utils/coercion_test.rb +33 -0
  42. data/test/redstruct/utils/inspectable_test.rb +31 -0
  43. data/test/redstruct/utils/iterable_test.rb +94 -0
  44. data/test/redstruct/utils/scriptable_test.rb +67 -0
  45. data/test/redstruct_test.rb +14 -0
  46. data/test/test_helper.rb +77 -1
  47. metadata +58 -26
  48. data/lib/redstruct/connection.rb +0 -47
  49. data/lib/redstruct/factory/creation.rb +0 -95
  50. data/lib/redstruct/factory/deserialization.rb +0 -7
  51. data/lib/redstruct/hls/lock.rb +0 -175
  52. data/lib/redstruct/hls/queue.rb +0 -29
  53. data/lib/redstruct/hls.rb +0 -2
  54. data/lib/redstruct/types/base.rb +0 -36
  55. data/lib/redstruct/types/counter.rb +0 -65
  56. data/lib/redstruct/types/hash.rb +0 -72
  57. data/lib/redstruct/types/list.rb +0 -76
  58. data/lib/redstruct/types/script.rb +0 -56
  59. data/lib/redstruct/types/set.rb +0 -96
  60. data/lib/redstruct/types/sorted_set.rb +0 -129
  61. data/lib/redstruct/types/string.rb +0 -64
  62. data/lib/redstruct/types/struct.rb +0 -58
  63. data/lib/releaser/logger.rb +0 -15
  64. data/lib/releaser/repository.rb +0 -32
  65. data/lib/tasks/release.rake +0 -49
  66. data/test/redstruct/restruct_test.rb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5f29df0c24d1d453a2a24b5e32cf563d284e08d2
4
- data.tar.gz: 26f083074180af497290197bb09a956d98e6c683
3
+ metadata.gz: 3e699e92aa673d70280aa80258169eaad2c8edad
4
+ data.tar.gz: f1de7e921d57040ae2e74d1cc1db2b481b25d5f8
5
5
  SHA512:
6
- metadata.gz: 0564b2207e6b03a6e7109db3c634dd74ce33d891bfbfa23f0ad8531482c7a9ca93a02935901ef8edb4c8abaa0a9c8b7a6ff5a277ff199a95dd2d79548d59374a
7
- data.tar.gz: b629606788ae08144cba7d154d9ecebe87d885f04fc74ea2472564a0b1e63559a9a50a8d44e14e583f9852341f622fe839095d3e461bf6697731462888d725b9
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 counters
38
- [x] Implement locks (blocking/non-blocking)
39
- [x] Implement queues
40
- [x] Implement basic types (set, string, list, hash)
41
- [ ] Implement SortedSet
42
- [ ] Implement stacks
43
- [ ] Discuss supporting pipelining vs. just using lua scripts (better interface for scripted extensions?)
44
- [ ] Design/discuss stored factory meta-data (i.e. keep track of created objects, clear said objects, etc.)
45
- [ ] Implement/redesign factory such that types, hls, etc., packages add methods as necessary (or not? discuss)
46
- [ ] Implement collections to leverage redis commands such as mget, mset, etc.
47
- [ ] Implement value transformers (read and write) to make reusing types with specially encoded objects easy
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
- t.files = ['lib/**/*.rb']
17
- t.options = ['--output-dir=./docs']
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] The Redis-rb connection pool to use
4
- attr_accessor :connection_pool
6
+ # @return [ConnectionPool, Redis] the default redis-rb connection, or a pool of said connections
7
+ attr_accessor :default_connection
5
8
 
6
- # @return [::String] Default namespace for factories
7
- attr_accessor :namespace
9
+ # @return [String] Default namespace for factories
10
+ attr_accessor :default_namespace
8
11
 
9
12
  def initialize
10
- @connection_pool = nil
11
- @namespace = nil
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Redstruct
2
4
  # Used to provide a single base error type to filter all errors coming from the gem.
3
5
  class Error < StandardError
@@ -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
@@ -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, Redstruct::Factory::Creation
7
- extend Redstruct::Factory::Deserialization
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 [Connection] The connection proxy to use when executing commands. Shared by all factory produced objects.
23
+ # @return [Redstruct::ConnectionProxy] connection proxy used to execute commands
10
24
  attr_reader :connection
11
25
 
12
- # @param [Redstruct::Connection] connection connection to use for all objects built by the factory
13
- # @param [ConnectionPool] pool pool to use to build a connection from if no connection param given
14
- # @param [::String] namespace all objects build from the factory will have their keys namespaced under this one
15
- # @return [Factory]
16
- def initialize(connection: nil, pool: nil, namespace: nil)
17
- namespace ||= Redstruct.config.namespace
18
-
19
- if connection.nil?
20
- pool ||= Redstruct.config.connection_pool
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 isolate/namespace
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 isolate(key)
34
- return @namespace.nil? || key.start_with?(@namespace) ? key : "#{@namespace}:#{key}"
35
- end
43
+ def prefix(key)
44
+ prefixed = key
45
+ prefixed = "#{@namespace}:#{key}" unless @namespace.empty? || key.start_with?("#{@namespace}:")
36
46
 
37
- # Iterates over the keys of this factory using the Redis scan command
38
- # For more about the scan command, see https://redis.io/commands/scan
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
- return enumerator unless block_given?
64
- return enumerator.each(&block)
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 delete_all(options = {})
71
- return each({ match: '*', count: 500, max: 1_000_000 }.merge(options)) do |keys|
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
- # :nocov:
66
+ # @!group Factory methods
77
67
 
78
- # Helper method for serialization
79
- def inspectable_attributes
80
- return { namespace: @namespace, script_cache: @script_cache.keys }
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
- # :nocov:
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