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.
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