kredis 0.1.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of kredis might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8c81a934c4d3b042045d49c96450e86c681529411fe5de467698d213f66bf21
4
- data.tar.gz: b5d81f606b35fa29ebccd67940c4fd8d6f01c59d5957b0f3fb5aabf06c0f3896
3
+ metadata.gz: bd8ef47e0ef8b83252c6dc0878bf81b6963a6fe959c1f81d776087a255798421
4
+ data.tar.gz: 04af1db077a8ba58769bb0614830c2fd1395a3a777df1d304c6e63f48753a5bb
5
5
  SHA512:
6
- metadata.gz: 8dff036d7c5dfecb9e755d991419622cc2456a7eb4f79ea7a1a317156fc5ba87525b339f92726a19062c43716c23dce707ab1ec80253852dc79fed7a48cfade3
7
- data.tar.gz: 64928a2e0d7c9ada0a51515dc8405339f053b5579f0cd57f109a52a38e23461a9be164db72c5fbb0eb0af8ff8fee3fb813e21c468b0d3e90c4a6a75747d7b777
6
+ metadata.gz: cae5b52d7156a5aae222962e9f328bcd954a09aae7d24ff272319ace142cec3ac78c18b7b6bf3a46403c00fbbf0561f9f60a554384195fd7208d88518f261ddf
7
+ data.tar.gz: 7b5e80312aa4daa05837697080e8ada67d9b1f59fda1f6902c9f9539caa745a703636f04d3576cb9dc86c7bbf23b8f74d0ba58b0beae7bd4aeb4e95abad180c6
data/README.md CHANGED
@@ -1,11 +1,170 @@
1
1
  # Kredis
2
2
 
3
- Keyed Redis encapsulates higher-level data structures around a single key, so you can interact with them as coherent objects rather than isolated procedural commands.
3
+ Kredis (Keyed Redis) encapsulates higher-level types and data structures around a single key, so you can interact with them as coherent objects rather than isolated procedural commands. These higher-level structures can be configured as attributes within Active Models and Active Records using a declarative DSL.
4
4
 
5
+ Kredis is configured using env-aware YAML files, using `Rails.application.config_for`, so you can locate the data structures on separate Redis instances, if you've reached a scale where a single shared instance is no longer sufficient.
5
6
 
6
- ## Development
7
+ Kredis provides namespacing support for keys such that you can safely run parallel testing against the data structures without different tests trampling each others data.
8
+
9
+
10
+ ## Examples
11
+
12
+ Kredis provides typed scalars for strings, integers, decimals, floats, booleans, datetimes, and JSON hashes:
13
+
14
+ ```ruby
15
+ string = Kredis.string "mystring"
16
+ string.value = "hello world!" # => SET mystring "hello world"
17
+ "hello world!" == string.value # => GET mystring
18
+
19
+ integer = Kredis.integer "myinteger"
20
+ integer.value = 5 # => SET myinteger "5"
21
+ 5 == integer.value # => GET myinteger
22
+
23
+ json = Kredis.json "myjson"
24
+ json.value = { "one" => 1, "two" => "2" } # => SET myjson "{\"one\":1,\"two\":\"2\"}"
25
+ { "one" => 1, "two" => "2" } == json.value # => GET myjson
26
+ ```
27
+
28
+ There are data structures for counters, enums, flags, lists, unique lists, sets, and slots:
29
+
30
+ ```ruby
31
+ list = Kredis.list "mylist"
32
+ list << "hello world!"
33
+ [ "hello world!" ] == list.elements
34
+
35
+ integer_list = Kredis.list "myintegerlist", typed: :integer
36
+ integer_list.append([ 1, 2, 3 ]) # => LPUSH myintegerlist "1" "2" "3"
37
+ integer_list << 4 # => LPUSH myintegerlist "4"
38
+ [ 1, 2, 3, 4 ] == integer_list.elements # LRANGE 0 -1
39
+
40
+ unique_list = Kredis.unique_list "myuniquelist"
41
+ unique_list.append(%w[ 2 3 4 ])
42
+ unique_list.prepend(%w[ 1 2 3 4 ])
43
+ unique_list.append([])
44
+ unique_list << "5"
45
+ unique_list.remove(3)
46
+ [ "1", "2", "4", "5" ] == unique_list.elements
47
+
48
+ set = Kredis.set "myset", typed: :datetime
49
+ set.add(DateTime.tomorrow, DateTime.yesterday) # => SADD myset "2021-02-03 00:00:00 +0100" "2021-02-01 00:00:00 +0100"
50
+ set << DateTime.tomorrow # => SADD myset "2021-02-03 00:00:00 +0100"
51
+ 2 == set.size # => SCARD myset
52
+ [ DateTime.tomorrow, DateTime.yesterday ] == set.elements # => SMEMBERS myset
53
+
54
+ head_count = Kredis.counter "headcount"
55
+ 0 == head_count.value # => GET "headcount"
56
+ head_count.increment
57
+ head_count.increment
58
+ head_count.decrement
59
+ 1 == head_count.value # => GET "headcount"
60
+
61
+ counter = Kredis.counter "mycounter", expires_in: 5.seconds
62
+ counter.increment by: 2 # => SETEX "mycounter" 900 0 + INCR "mycounter" 2
63
+ 2 == counter.value # => GET "mycounter"
64
+ sleep 6.seconds
65
+ 0 == counter.value # => GET "mycounter"
66
+
67
+ cycle = Kredis.cycle "mycycle", values: %i[ one two three ]
68
+ :one == cycle.value
69
+ cycle.next
70
+ :two == cycle.value
71
+ cycle.next
72
+ :three == cycle.value
73
+ cycle.next
74
+ :one == cycle.value
75
+
76
+ enum = Kredis.enum "myenum", values: %w[ one two three ], default: "one"
77
+ "one" == enum.value
78
+ true == enum.one?
79
+ enum.value = "two"
80
+ "two" == enum.value
81
+ enum.value = "four"
82
+ "two" == enum.value
83
+ enum.reset
84
+ "one" == enum.value
85
+
86
+ slots = Kredis.slots "myslots", available: 3
87
+ true == slots.available?
88
+ slots.reserve
89
+ true == slots.available?
90
+ slots.reserve
91
+ true == slots.available?
92
+ slots.reserve
93
+ true == slots.available?
94
+ slots.reserve
95
+ false == slots.available?
96
+ slots.release
97
+ true == slots.available?
98
+ slots.reset
99
+
100
+ flag = Kredis.flag "myflag"
101
+ false == flag.marked?
102
+ flag.mark
103
+ true == flag.marked?
104
+ flag.remove
105
+ false == flag.marked?
106
+
107
+ flag.mark(expires_in: 1.second)
108
+ true == flag.marked?
109
+ sleep 0.5.seconds
110
+ true == flag.marked?
111
+ sleep 0.6.seconds
112
+ false == flag.marked?
113
+ ```
114
+
115
+ And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`:
116
+
117
+ ```ruby
118
+ one_string = Kredis.string "mystring"
119
+ two_string = Kredis.string "mystring", config: :secondary
120
+
121
+ one_string.value = "just on shared"
122
+ two_string.value != one_string.value
123
+ ```
124
+
125
+ You can use all these structures in models:
126
+
127
+ ```ruby
128
+ class Person < ApplicationRecord
129
+ kredis_list :names
130
+ kredis_list :names_with_custom_key, key: ->(p) { "person:#{p.id}:names_customized" }
131
+ kredis_unique_list :skills, limit: 2
132
+ kredis_enum :morning, values: %w[ bright blue black ], default: "bright"
133
+ end
134
+
135
+ person = Person.find(5)
136
+ person.names.append "David", "Heinemeier", "Hansson" # => SADD person:5:names "David" "Heinemeier" "Hansson"
137
+ true == person.morning.bright?
138
+ person.morning.value = "blue"
139
+ true == person.morning.blue?
140
+ ```
141
+
142
+
143
+ ## Installation
144
+
145
+ 1. Add the `kredis` gem to your Gemfile: `gem 'kredis'`
146
+ 2. Run `./bin/bundle install`
147
+ 3. Add a default configuration under `config/redis/shared.yml`
148
+
149
+ A default configuration can look like this for `config/redis/shared.yml`:
150
+
151
+ ```yaml
152
+ production: &production
153
+ host: <%= ENV.fetch("REDIS_SHARED_HOST", "127.0.0.1") %>
154
+ port: <%= ENV.fetch("REDIS_SHARED_PORT", "6379") %>
155
+ timeout: 1
156
+
157
+ development: &development
158
+ host: <%= ENV.fetch("REDIS_SHARED_HOST", "127.0.0.1") %>
159
+ port: <%= ENV.fetch("REDIS_SHARED_PORT", "6379") %>
160
+ timeout: 1
161
+
162
+ test:
163
+ <<: *development
164
+ ```
165
+
166
+ Additional configurations can be added under `config/redis/*.yml` and referenced when a type is created, e.g. `Kredis.string("mystring", config: :strings)` would lookup `config/redis/strings.yml`. Under the hood `Kredis.configured_for` is called which'll pass the configuration on to `Redis.new`.
7
167
 
8
- ...
9
168
 
10
169
  ## License
11
170
 
data/lib/kredis.rb CHANGED
@@ -1,14 +1,24 @@
1
- require "kredis/railtie"
1
+ require "active_support"
2
+ require "active_support/core_ext/module/attribute_accessors"
3
+
4
+ require "kredis/version"
2
5
 
3
6
  require "kredis/connections"
7
+ require "kredis/namespace"
8
+ require "kredis/type_casting"
4
9
  require "kredis/types"
5
10
  require "kredis/attributes"
6
- require "kredis/namespace"
11
+
12
+ require "kredis/railtie" if defined?(Rails::Railtie)
7
13
 
8
14
  module Kredis
9
- include Connections
10
- include Namespace
11
- include Types
15
+ include Connections, Namespace, TypeCasting, Types
12
16
 
13
17
  extend self
18
+
19
+ mattr_accessor :logger
20
+
21
+ def redis(config: :shared)
22
+ configured_for(config)
23
+ end
14
24
  end
@@ -2,49 +2,86 @@ module Kredis::Attributes
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  class_methods do
5
- def kredis_list(name, config: :shared)
6
- ivar_symbol = :"@#{name}_kredis_list"
7
-
8
- define_method(name) do
9
- if instance_variable_defined?(ivar_symbol)
10
- instance_variable_get(ivar_symbol)
11
- else
12
- instance_variable_set(ivar_symbol, Kredis.list(kredis_key_for_attribute(name), config: config))
13
- end
14
- end
5
+ def kredis_proxy(name, key: nil, config: :shared)
6
+ kredis_connection_with __method__, name, key, config: config
15
7
  end
16
8
 
17
- def kredis_unique_list(name, limit: nil, config: :shared)
18
- ivar_symbol = :"@#{name}_kredis_unique_list"
9
+ def kredis_string(name, key: nil, config: :shared)
10
+ kredis_connection_with __method__, name, key, config: config
11
+ end
19
12
 
20
- define_method(name) do
21
- if instance_variable_defined?(ivar_symbol)
22
- instance_variable_get(ivar_symbol)
23
- else
24
- instance_variable_set(ivar_symbol, Kredis.unique_list(kredis_key_for_attribute(name), limit: limit, config: config))
25
- end
26
- end
13
+ def kredis_integer(name, key: nil, config: :shared)
14
+ kredis_connection_with __method__, name, key, config: config
27
15
  end
28
16
 
29
- def kredis_flag(name, config: :shared)
30
- ivar_symbol = :"@#{name}_kredis_flag"
17
+ def kredis_datetime(name, key: nil, config: :shared)
18
+ kredis_connection_with __method__, name, key, config: config
19
+ end
31
20
 
32
- define_method(name) do
33
- if instance_variable_defined?(ivar_symbol)
34
- instance_variable_get(ivar_symbol)
35
- else
36
- instance_variable_set(ivar_symbol, Kredis.flag(kredis_key_for_attribute(name), config: config))
37
- end
38
- end
21
+ def kredis_flag(name, key: nil, config: :shared)
22
+ kredis_connection_with __method__, name, key, config: config
39
23
 
40
24
  define_method("#{name}?") do
41
- instance_variable_defined?(ivar_symbol) && instance_variable_get(ivar_symbol).marked?
25
+ send(name).marked?
42
26
  end
43
27
  end
28
+
29
+ def kredis_enum(name, key: nil, values:, default:, config: :shared)
30
+ kredis_connection_with __method__, name, key, values: values, default: default, config: config
31
+ end
32
+
33
+ def kredis_json(name, key: nil, config: :shared)
34
+ kredis_connection_with __method__, name, key, config: config
35
+ end
36
+
37
+ def kredis_list(name, key: nil, typed: :string, config: :shared)
38
+ kredis_connection_with __method__, name, key, typed: typed, config: config
39
+ end
40
+
41
+ def kredis_unique_list(name, limit: nil, key: nil, typed: :string, config: :shared)
42
+ kredis_connection_with __method__, name, key, limit: limit, typed: typed, config: config
43
+ end
44
+
45
+ def kredis_set(name, key: nil, typed: :string, config: :shared)
46
+ kredis_connection_with __method__, name, key, typed: typed, config: config
47
+ end
48
+
49
+ def kredis_slot(name, key: nil, config: :shared)
50
+ kredis_connection_with __method__, name, key, config: config
51
+ end
52
+
53
+ def kredis_slots(name, available:, key: nil, config: :shared)
54
+ kredis_connection_with __method__, name, key, available: available, config: config
55
+ end
56
+
57
+ private
58
+ def kredis_connection_with(method, name, key, **options)
59
+ ivar_symbol = :"@#{name}_#{method}"
60
+ type = method.to_s.sub("kredis_", "")
61
+
62
+ define_method(name) do
63
+ if instance_variable_defined?(ivar_symbol)
64
+ instance_variable_get(ivar_symbol)
65
+ else
66
+ instance_variable_set(ivar_symbol, Kredis.send(type, kredis_key_evaluated(key) || kredis_key_for_attribute(name), **options))
67
+ end
68
+ end
69
+ end
44
70
  end
45
71
 
46
72
  private
73
+ def kredis_key_evaluated(key)
74
+ case key
75
+ when String then key
76
+ when Proc then key.call(self)
77
+ end
78
+ end
79
+
47
80
  def kredis_key_for_attribute(name)
48
- "#{self.class.name.tableize.gsub("/", ":")}:#{id}:#{name}"
81
+ "#{self.class.name.tableize.gsub("/", ":")}:#{extract_kredis_id}:#{name}"
82
+ end
83
+
84
+ def extract_kredis_id
85
+ try(:id) or raise NotImplementedError, "kredis needs a unique id, either implement an id method or pass a custom key."
49
86
  end
50
87
  end
@@ -5,10 +5,21 @@ module Kredis::Connections
5
5
  mattr_accessor :configurator
6
6
 
7
7
  def configured_for(name)
8
- connections[name] ||= Redis.new configurator.config_for("redis/#{name}")
8
+ connections[name] ||= begin
9
+ logger&.info "[Kredis] Connected to #{name}"
10
+ Redis.new configurator.config_for("redis/#{name}")
11
+ end
9
12
  end
10
13
 
11
14
  def clear_all
12
- connections.each_value(&:flushdb)
15
+ logger&.info "[Kredis] Connections all cleared"
16
+ connections.each_value do |connection|
17
+ if Kredis.namespace
18
+ keys = connection.keys("#{Kredis.namespace}:*")
19
+ connection.del keys if keys.any?
20
+ else
21
+ connection.flushdb
22
+ end
23
+ end
13
24
  end
14
25
  end
@@ -0,0 +1,52 @@
1
+ require "active_support/core_ext/module/delegation"
2
+
3
+ class Kredis::Migration
4
+ singleton_class.delegate :migrate_all, :migrate, :delete_all, to: :new
5
+
6
+ def initialize(config = :shared)
7
+ @redis = Kredis.configured_for config
8
+ # TODO: Replace script loading with `copy` command once Redis 6.2+ is the minimum supported version.
9
+ @copy_sha = @redis.script "load", "redis.call('SETNX', KEYS[2], redis.call('GET', KEYS[1])); return 1;"
10
+ end
11
+
12
+ def migrate_all(key_pattern)
13
+ each_key_batch_matching(key_pattern) do |keys|
14
+ keys.each do |key|
15
+ ids = key.scan(/\d+/).map(&:to_i)
16
+ migrate from: key, to: yield(key, *ids)
17
+ end
18
+ end
19
+ end
20
+
21
+ def migrate(from:, to:)
22
+ namespaced_to = Kredis.namespaced_key(to)
23
+
24
+ if to.present? && from != namespaced_to
25
+ log_migration "Migrating key #{from} to #{namespaced_to}"
26
+ @redis.evalsha @copy_sha, keys: [ from, namespaced_to ]
27
+ else
28
+ log_migration "Skipping blank/unaltered migration key #{from} → #{to}"
29
+ end
30
+ end
31
+
32
+ def delete_all(key_pattern)
33
+ each_key_batch_matching(key_pattern) do |keys|
34
+ @redis.del *keys
35
+ end
36
+ end
37
+
38
+ private
39
+ SCAN_BATCH_SIZE = 1_000
40
+
41
+ def each_key_batch_matching(key_pattern, &block)
42
+ cursor = "0"
43
+ begin
44
+ cursor, keys = @redis.scan(cursor, match: key_pattern, count: SCAN_BATCH_SIZE)
45
+ @redis.pipelined { yield keys }
46
+ end until cursor == "0"
47
+ end
48
+
49
+ def log_migration(message)
50
+ Kredis.logger&.debug "[Kredis Migration] #{message}"
51
+ end
52
+ end
@@ -1,29 +1,29 @@
1
- require "rails/railtie"
1
+ class Kredis::Railtie < ::Rails::Railtie
2
+ config.kredis = ActiveSupport::OrderedOptions.new
2
3
 
3
- module Kredis
4
- class Railtie < ::Rails::Railtie
5
- config.kredis = ActiveSupport::OrderedOptions.new
6
- config.eager_load_namespaces << Kredis
7
-
8
- initializer "kredis.testing" do
9
- ActiveSupport.on_load(:active_support_test_case) do
10
- parallelize_setup { |worker| Kredis.namespace = "test-#{worker}" }
11
- parallelize_teardown { Kredis.clear_all }
12
- end
4
+ initializer "kredis.testing" do
5
+ ActiveSupport.on_load(:active_support_test_case) do
6
+ parallelize_setup { |worker| Kredis.namespace = "test-#{worker}" }
7
+ teardown { Kredis.clear_all }
13
8
  end
9
+ end
14
10
 
15
- initializer "kredis.configurator" do
16
- Kredis.configurator = Rails.application
17
- end
11
+ initializer "kredis.logger" do
12
+ Kredis.logger = config.kredis.logger || Rails.logger
13
+ end
18
14
 
19
- initializer "kredis.attributes" do
20
- ActiveSupport.on_load(:active_model) do
21
- ActiveModel::Base.send :include, Kredis::Attributes
22
- end
15
+ initializer "kredis.configurator" do
16
+ Kredis.configurator = Rails.application
17
+ end
18
+
19
+ initializer "kredis.attributes" do
20
+ # No load hook for Active Model, just defer until after initialization.
21
+ config.after_initialize do
22
+ ActiveModel::Model.include Kredis::Attributes if defined?(ActiveModel::Model)
23
+ end
23
24
 
24
- ActiveSupport.on_load(:active_record) do
25
- ActiveRecord::Base.send :include, Kredis::Attributes
26
- end
25
+ ActiveSupport.on_load(:active_record) do
26
+ include Kredis::Attributes
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,48 @@
1
+ module Kredis::TypeCasting
2
+ class InvalidType < StandardError; end
3
+
4
+ VALID_TYPES = %i[ string integer decimal float boolean datetime json ]
5
+
6
+ def type_to_string(value)
7
+ case value
8
+ when nil
9
+ ""
10
+ when Integer
11
+ value.to_s
12
+ when BigDecimal
13
+ value.to_d
14
+ when Float
15
+ value.to_s
16
+ when TrueClass, FalseClass
17
+ value ? "t" : "f"
18
+ when Time, DateTime, ActiveSupport::TimeWithZone
19
+ value.iso8601(9)
20
+ when Hash
21
+ JSON.dump(value)
22
+ else
23
+ value
24
+ end
25
+ end
26
+
27
+ def string_to_type(value, type)
28
+ raise InvalidType if type && !type.in?(VALID_TYPES)
29
+
30
+ case type
31
+ when nil, :string then value
32
+ when :integer then value.to_i
33
+ when :decimal then value.to_d
34
+ when :float then value.to_f
35
+ when :boolean then value == "t" ? true : false
36
+ when :datetime then Time.iso8601(value)
37
+ when :json then JSON.load(value)
38
+ end if value.present?
39
+ end
40
+
41
+ def types_to_strings(values)
42
+ Array(values).flatten.map { |value| type_to_string(value) }
43
+ end
44
+
45
+ def strings_to_types(values, type)
46
+ Array(values).flatten.map { |value| string_to_type(value, type) }
47
+ end
48
+ end
data/lib/kredis/types.rb CHANGED
@@ -1,32 +1,88 @@
1
1
  module Kredis::Types
2
- def keyed(key, config: :shared)
3
- Kredis::Proxy.new configured_for(config), namespaced_key(key)
2
+ def proxy(key, config: :shared)
3
+ Proxy.new configured_for(config), namespaced_key(key)
4
4
  end
5
5
 
6
- def list(key, config: :shared)
7
- List.new configured_for(config), namespaced_key(key)
6
+
7
+ def scalar(key, typed: :string, default: nil, config: :shared)
8
+ Scalar.new configured_for(config), namespaced_key(key), typed: typed, default: default
9
+ end
10
+
11
+ def string(key, default: nil, config: :shared)
12
+ Scalar.new configured_for(config), namespaced_key(key), typed: :string, default: default
13
+ end
14
+
15
+ def integer(key, default: nil, config: :shared)
16
+ Scalar.new configured_for(config), namespaced_key(key), typed: :integer, default: default
17
+ end
18
+
19
+ def decimal(key, default: nil, config: :shared)
20
+ Scalar.new configured_for(config), namespaced_key(key), typed: :decimal, default: default
21
+ end
22
+
23
+ def float(key, default: nil, config: :shared)
24
+ Scalar.new configured_for(config), namespaced_key(key), typed: :float, default: default
25
+ end
26
+
27
+ def boolean(key, default: nil, config: :shared)
28
+ Scalar.new configured_for(config), namespaced_key(key), typed: :boolean, default: default
29
+ end
30
+
31
+ def datetime(key, default: nil, config: :shared)
32
+ Scalar.new configured_for(config), namespaced_key(key), typed: :datetime, default: default
8
33
  end
9
34
 
10
- def unique_list(key, limit: nil, config: :shared)
11
- UniqueList.new configured_for(config), namespaced_key(key), limit: limit
35
+ def json(key, default: nil, config: :shared)
36
+ Scalar.new configured_for(config), namespaced_key(key), typed: :json, default: default
12
37
  end
13
38
 
39
+
14
40
  def counter(key, expires_in: nil, config: :shared)
15
41
  Counter.new configured_for(config), namespaced_key(key), expires_in: expires_in
16
42
  end
17
43
 
44
+ def cycle(key, values:, expires_in: nil, config: :shared)
45
+ Cycle.new configured_for(config), namespaced_key(key), values: values, expires_in: expires_in
46
+ end
47
+
18
48
  def flag(key, config: :shared)
19
49
  Flag.new configured_for(config), namespaced_key(key)
20
50
  end
21
51
 
22
- def mutex(key, expires_in: nil, config: :shared)
23
- Mutex.new configured_for(config), namespaced_key(key), expires_in: expires_in
52
+ def enum(key, values:, default:, config: :shared)
53
+ Enum.new configured_for(config), namespaced_key(key), values: values, default: default
54
+ end
55
+
56
+ def list(key, typed: :string, config: :shared)
57
+ List.new configured_for(config), namespaced_key(key), typed: typed
58
+ end
59
+
60
+ def unique_list(key, typed: :string, limit: nil, config: :shared)
61
+ UniqueList.new configured_for(config), namespaced_key(key), typed: typed, limit: limit
62
+ end
63
+
64
+ def set(key, typed: :string, config: :shared)
65
+ Set.new configured_for(config), namespaced_key(key), typed: typed
66
+ end
67
+
68
+ def slot(key, config: :shared)
69
+ Slots.new configured_for(config), namespaced_key(key), available: 1
70
+ end
71
+
72
+ def slots(key, available:, config: :shared)
73
+ Slots.new configured_for(config), namespaced_key(key), available: available
24
74
  end
25
75
  end
26
76
 
27
- require "kredis/proxy"
28
- require "kredis/types/list"
29
- require "kredis/types/unique_list"
77
+ require "kredis/types/proxy"
78
+ require "kredis/types/proxying"
79
+
80
+ require "kredis/types/scalar"
30
81
  require "kredis/types/counter"
82
+ require "kredis/types/cycle"
31
83
  require "kredis/types/flag"
32
- require "kredis/types/mutex"
84
+ require "kredis/types/enum"
85
+ require "kredis/types/list"
86
+ require "kredis/types/unique_list"
87
+ require "kredis/types/set"
88
+ require "kredis/types/slots"
@@ -1,17 +1,27 @@
1
- class Kredis::Types::Counter < Kredis::Proxy
2
- def initialize(redis, key, expires_in: nil)
3
- @expires_in = expires_in
4
- super redis, key
5
- end
1
+ class Kredis::Types::Counter < Kredis::Types::Proxying
2
+ proxying :multi, :set, :incrby, :decrby, :get, :del
3
+
4
+ attr_accessor :expires_in
6
5
 
7
6
  def increment(by: 1)
8
7
  multi do
9
- set 0, ex: @expires_in, nx: true
8
+ set 0, ex: expires_in, nx: true
10
9
  incrby by
11
10
  end
12
11
  end
13
12
 
13
+ def decrement(by: 1)
14
+ multi do
15
+ set 0, ex: expires_in, nx: true
16
+ decrby by
17
+ end
18
+ end
19
+
14
20
  def value
15
21
  get.to_i
16
22
  end
23
+
24
+ def reset
25
+ del
26
+ end
17
27
  end
@@ -0,0 +1,13 @@
1
+ class Kredis::Types::Cycle < Kredis::Types::Counter
2
+ attr_accessor :values
3
+
4
+ alias index value
5
+
6
+ def value
7
+ values[index]
8
+ end
9
+
10
+ def next
11
+ set (index + 1) % values.size
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ class Kredis::Types::Enum < Kredis::Types::Proxying
2
+ proxying :set, :get, :del
3
+
4
+ attr_accessor :values, :default
5
+
6
+ def initialize(...)
7
+ super
8
+ define_predicates_for_values
9
+ end
10
+
11
+ def value=(value)
12
+ if validated_choice = value.presence_in(values)
13
+ set validated_choice
14
+ end
15
+ end
16
+
17
+ def value
18
+ get || default
19
+ end
20
+
21
+ def reset
22
+ del
23
+ end
24
+
25
+ private
26
+ def define_predicates_for_values
27
+ values.each do |defined_value|
28
+ define_singleton_method("#{defined_value}?") { value == defined_value }
29
+ end
30
+ end
31
+ end
@@ -1,6 +1,8 @@
1
- class Kredis::Types::Flag < Kredis::Proxy
2
- def mark
3
- set 1
1
+ class Kredis::Types::Flag < Kredis::Types::Proxying
2
+ proxying :set, :exists?, :del
3
+
4
+ def mark(expires_in: nil)
5
+ set 1, ex: expires_in
4
6
  end
5
7
 
6
8
  def marked?
@@ -1,17 +1,23 @@
1
- class Kredis::Types::List < Kredis::Proxy
1
+ class Kredis::Types::List < Kredis::Types::Proxying
2
+ proxying :lrange, :lrem, :lpush, :rpush
3
+
4
+ attr_accessor :typed
5
+
2
6
  def elements
3
- lrange(0, -1) || []
7
+ strings_to_types(lrange(0, -1) || [], typed)
4
8
  end
9
+ alias to_a elements
5
10
 
6
- def remove(elements)
7
- Array(elements).each { |element| lrem 0, element }
11
+ def remove(*elements)
12
+ types_to_strings(elements).each { |element| lrem 0, element }
8
13
  end
9
14
 
10
- def prepend(elements)
11
- lpush elements if Array(elements).any?
15
+ def prepend(*elements)
16
+ lpush types_to_strings(elements) if elements.flatten.any?
12
17
  end
13
18
 
14
- def append(elements)
15
- rpush elements if Array(elements).any?
19
+ def append(*elements)
20
+ rpush types_to_strings(elements) if elements.flatten.any?
16
21
  end
22
+ alias << append
17
23
  end
@@ -0,0 +1,31 @@
1
+ class Kredis::Types::Proxy
2
+ require_relative "proxy/failsafe"
3
+ include Failsafe
4
+
5
+ attr_accessor :redis, :key
6
+
7
+ def initialize(redis, key, **options)
8
+ @redis, @key = redis, key
9
+ options.each { |key, value| send("#{key}=", value) }
10
+ end
11
+
12
+ def multi(...)
13
+ redis.multi(...)
14
+ end
15
+
16
+ def method_missing(method, *args, **kwargs)
17
+ failsafe do
18
+ Kredis.logger&.debug log_message(method, *args, **kwargs)
19
+ redis.public_send method, key, *args, **kwargs
20
+ end
21
+ end
22
+
23
+ private
24
+ def log_message(method, *args, **kwargs)
25
+ args = args.flatten.reject(&:blank?).presence
26
+ kwargs = kwargs.reject { |_k, v| v.blank? }.presence
27
+ type_name = self.class.name.split("::").last
28
+
29
+ "[Kredis #{type_name}] #{method.upcase} #{key} #{args&.inspect} #{kwargs&.inspect}".chomp
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ module Kredis::Types::Proxy::Failsafe
2
+ def initialize(*)
3
+ super
4
+ @fail_safe_suppressed = false
5
+ end
6
+
7
+ def failsafe
8
+ yield
9
+ rescue Redis::BaseError
10
+ raise if fail_safe_suppressed?
11
+ end
12
+
13
+ def suppress_failsafe_with(returning: nil)
14
+ old_fail_safe_suppressed, @fail_safe_suppressed = @fail_safe_suppressed, true
15
+ yield
16
+ rescue Redis::BaseError
17
+ returning
18
+ ensure
19
+ @fail_safe_suppressed = old_fail_safe_suppressed
20
+ end
21
+
22
+ private
23
+ def fail_safe_suppressed?
24
+ @fail_safe_suppressed
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ require "active_support/core_ext/module/delegation"
2
+
3
+ class Kredis::Types::Proxying
4
+ attr_accessor :proxy, :redis, :key
5
+
6
+ def self.proxying(*commands)
7
+ delegate *commands, to: :proxy
8
+ end
9
+
10
+ def initialize(redis, key, **options)
11
+ @redis, @key = redis, key
12
+ @proxy = Kredis::Types::Proxy.new(redis, key)
13
+ options.each { |key, value| send("#{key}=", value) }
14
+ end
15
+
16
+ def failsafe(returning: nil, &block)
17
+ proxy.suppress_failsafe_with(returning: returning, &block)
18
+ end
19
+
20
+ private
21
+ delegate :type_to_string, :string_to_type, :types_to_strings, :strings_to_types, to: :Kredis
22
+ end
@@ -0,0 +1,25 @@
1
+ class Kredis::Types::Scalar < Kredis::Types::Proxying
2
+ proxying :set, :get, :exists?, :del
3
+
4
+ attr_accessor :typed, :default
5
+
6
+ def value=(value)
7
+ set type_to_string(value)
8
+ end
9
+
10
+ def value
11
+ string_to_type(get, typed) || default
12
+ end
13
+
14
+ def to_s
15
+ get || default&.to_s
16
+ end
17
+
18
+ def assigned?
19
+ exists?
20
+ end
21
+
22
+ def clear
23
+ del
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ class Kredis::Types::Set < Kredis::Types::Proxying
2
+ proxying :smembers, :sadd, :srem, :multi, :del, :sismember, :scard, :spop
3
+
4
+ attr_accessor :typed
5
+
6
+ def members
7
+ strings_to_types(smembers || [], typed).sort
8
+ end
9
+ alias to_a members
10
+
11
+ def add(*members)
12
+ sadd types_to_strings(members) if members.flatten.any?
13
+ end
14
+ alias << add
15
+
16
+ def remove(*members)
17
+ srem types_to_strings(members) if members.flatten.any?
18
+ end
19
+
20
+ def replace(*members)
21
+ multi do
22
+ del
23
+ add members
24
+ end
25
+ end
26
+
27
+ def include?(member)
28
+ sismember type_to_string(member)
29
+ end
30
+
31
+ def size
32
+ scard.to_i
33
+ end
34
+
35
+ def take
36
+ spop
37
+ end
38
+
39
+ def clear
40
+ del
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ class Kredis::Types::Slots < Kredis::Types::Proxying
2
+ class NotAvailable < StandardError; end
3
+
4
+ proxying :incr, :decr, :get, :del
5
+
6
+ attr_accessor :available
7
+
8
+ def reserve
9
+ failsafe returning: false do
10
+ if block_given?
11
+ begin
12
+ if reserve
13
+ yield
14
+ true
15
+ else
16
+ false
17
+ end
18
+ ensure
19
+ release
20
+ end
21
+ else
22
+ if incr <= available
23
+ true
24
+ else
25
+ release
26
+ false
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def release
33
+ decr
34
+ end
35
+
36
+ def available?
37
+ failsafe returning: false do
38
+ get.to_i < available
39
+ end
40
+ end
41
+
42
+ def reset
43
+ del
44
+ end
45
+ end
@@ -1,23 +1,23 @@
1
1
  # You'd normally call this a set, but Redis already has another data type for that
2
2
  class Kredis::Types::UniqueList < Kredis::Types::List
3
- def initialize(redis, key, limit: nil)
4
- @limit = limit
5
- super redis, key
6
- end
3
+ proxying :multi, :ltrim
4
+
5
+ attr_accessor :typed, :limit
7
6
 
8
7
  def prepend(elements)
9
8
  multi do
10
9
  remove elements
11
10
  super
12
- ltrim 0, (@limit - 1) if @limit
13
- end if Array(elements).any?
11
+ ltrim 0, (limit - 1) if limit
12
+ end if Array(elements).flatten.any?
14
13
  end
15
14
 
16
15
  def append(elements)
17
16
  multi do
18
17
  remove elements
19
18
  super
20
- ltrim (@limit - 1), -1 if @limit
21
- end if Array(elements).any?
19
+ ltrim (limit - 1), -1 if limit
20
+ end if Array(elements).flatten.any?
22
21
  end
22
+ alias << append
23
23
  end
@@ -1,3 +1,3 @@
1
1
  module Kredis
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.3"
3
3
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kredis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasper Timm Hansen
8
8
  - David Heinemeier Hansson
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-01-30 00:00:00.000000000 Z
12
+ date: 2021-02-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -31,15 +31,15 @@ dependencies:
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: '4.0'
34
+ version: '4.2'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: '4.0'
42
- description:
41
+ version: '4.2'
42
+ description:
43
43
  email: david@hey.com
44
44
  executables: []
45
45
  extensions: []
@@ -50,21 +50,29 @@ files:
50
50
  - lib/kredis.rb
51
51
  - lib/kredis/attributes.rb
52
52
  - lib/kredis/connections.rb
53
+ - lib/kredis/migration.rb
53
54
  - lib/kredis/namespace.rb
54
- - lib/kredis/proxy.rb
55
55
  - lib/kredis/railtie.rb
56
+ - lib/kredis/type_casting.rb
56
57
  - lib/kredis/types.rb
57
58
  - lib/kredis/types/counter.rb
59
+ - lib/kredis/types/cycle.rb
60
+ - lib/kredis/types/enum.rb
58
61
  - lib/kredis/types/flag.rb
59
62
  - lib/kredis/types/list.rb
60
- - lib/kredis/types/mutex.rb
63
+ - lib/kredis/types/proxy.rb
64
+ - lib/kredis/types/proxy/failsafe.rb
65
+ - lib/kredis/types/proxying.rb
66
+ - lib/kredis/types/scalar.rb
67
+ - lib/kredis/types/set.rb
68
+ - lib/kredis/types/slots.rb
61
69
  - lib/kredis/types/unique_list.rb
62
70
  - lib/kredis/version.rb
63
71
  homepage: https://github.com/rails/kredis
64
72
  licenses:
65
73
  - MIT
66
74
  metadata: {}
67
- post_install_message:
75
+ post_install_message:
68
76
  rdoc_options: []
69
77
  require_paths:
70
78
  - lib
@@ -72,15 +80,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
72
80
  requirements:
73
81
  - - ">="
74
82
  - !ruby/object:Gem::Version
75
- version: 2.6.0
83
+ version: 2.7.0
76
84
  required_rubygems_version: !ruby/object:Gem::Requirement
77
85
  requirements:
78
86
  - - ">="
79
87
  - !ruby/object:Gem::Version
80
88
  version: '0'
81
89
  requirements: []
82
- rubygems_version: 3.1.2
83
- signing_key:
90
+ rubygems_version: 3.2.5
91
+ signing_key:
84
92
  specification_version: 4
85
93
  summary: Higher-level data structures built on Redis.
86
94
  test_files: []
data/lib/kredis/proxy.rb DELETED
@@ -1,13 +0,0 @@
1
- class Kredis::Proxy
2
- def initialize(redis, key)
3
- @redis, @key = redis, key
4
- end
5
-
6
- def multi(...)
7
- @redis.multi(...)
8
- end
9
-
10
- def method_missing(method, *args, **kwargs)
11
- @redis.public_send method, @key, *args, **kwargs
12
- end
13
- end
@@ -1,25 +0,0 @@
1
- class Kredis::Types::Mutex < Kredis::Proxy
2
- def initialize(redis, key, expires_in: nil)
3
- @expires_in = expires_in
4
- super redis, key
5
- end
6
-
7
- def lock
8
- set 1, ex: @expires_in
9
- end
10
-
11
- def unlock
12
- del
13
- end
14
-
15
- def locked?
16
- get
17
- end
18
-
19
- def synchronize
20
- lock
21
- yield
22
- ensure
23
- unlock
24
- end
25
- end