kredis 0.2.3 → 1.0.0

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: bd8ef47e0ef8b83252c6dc0878bf81b6963a6fe959c1f81d776087a255798421
4
- data.tar.gz: 04af1db077a8ba58769bb0614830c2fd1395a3a777df1d304c6e63f48753a5bb
3
+ metadata.gz: 6977b6ccbe9dba3574b4f0fe0c107fde3e1ff8a4e8a52f46076fe8df3acab2db
4
+ data.tar.gz: b3b9fb9c0e82a898168c6a9442cba36938bc4f7b9046dca16be052d28074c3ef
5
5
  SHA512:
6
- metadata.gz: cae5b52d7156a5aae222962e9f328bcd954a09aae7d24ff272319ace142cec3ac78c18b7b6bf3a46403c00fbbf0561f9f60a554384195fd7208d88518f261ddf
7
- data.tar.gz: 7b5e80312aa4daa05837697080e8ada67d9b1f59fda1f6902c9f9539caa745a703636f04d3576cb9dc86c7bbf23b8f74d0ba58b0beae7bd4aeb4e95abad180c6
6
+ metadata.gz: aa0715d6e188a7261f2f4c27f13a1b4dc919c5bf339c29435836693c3ae8df19f63ae54ff0d0ca28712f984a238df124bdc06ab268b85c88fd28179ae1534560
7
+ data.tar.gz: 9bb31956f1c666e64fc50c12a35b1a9ebefad8ec7f139b67c73033e745c48b8f271736b533c91eb6a4c84a892f541edb634688663219bd1285a1c3aafb996efd
data/README.md CHANGED
@@ -20,6 +20,23 @@ integer = Kredis.integer "myinteger"
20
20
  integer.value = 5 # => SET myinteger "5"
21
21
  5 == integer.value # => GET myinteger
22
22
 
23
+ decimal = Kredis.decimal "mydecimal" # accuracy!
24
+ decimal.value = "%.47f" % (1.0/10) # => SET mydecimal "0.10000000000000000555111512312578270211815834045"
25
+ BigDecimal("0.10000000000000000555111512312578270211815834045e0") == decimal.value # => GET mydecimal
26
+
27
+ float = Kredis.float "myfloat" # speed!
28
+ float.value = 1.0/10 # => SET myfloat "0.1"
29
+ 0.1 == float.value # => GET myfloat
30
+
31
+ boolean = Kredis.boolean "myboolean"
32
+ boolean.value = true # => SET myboolean "t"
33
+ true == boolean.value # => GET myboolean
34
+
35
+ datetime = Kredis.datetime "mydatetime"
36
+ memoized_midnight = Time.zone.now.midnight
37
+ datetime.value = memoized_midnight # SET mydatetime "2021-07-27T00:00:00.000000000Z"
38
+ memoized_midnight == datetime.value # => GET mydatetime
39
+
23
40
  json = Kredis.json "myjson"
24
41
  json.value = { "one" => 1, "two" => "2" } # => SET myjson "{\"one\":1,\"two\":\"2\"}"
25
42
  { "one" => 1, "two" => "2" } == json.value # => GET myjson
@@ -29,87 +46,111 @@ There are data structures for counters, enums, flags, lists, unique lists, sets,
29
46
 
30
47
  ```ruby
31
48
  list = Kredis.list "mylist"
32
- list << "hello world!"
33
- [ "hello world!" ] == list.elements
49
+ list << "hello world!" # => RPUSH mylist "hello world!"
50
+ [ "hello world!" ] == list.elements # => LRANGE mylist 0, -1
34
51
 
35
52
  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
53
+ integer_list.append([ 1, 2, 3 ]) # => RPUSH myintegerlist "1" "2" "3"
54
+ integer_list << 4 # => RPUSH myintegerlist "4"
55
+ [ 1, 2, 3, 4 ] == integer_list.elements # => LRANGE myintegerlist 0 -1
39
56
 
40
57
  unique_list = Kredis.unique_list "myuniquelist"
41
- unique_list.append(%w[ 2 3 4 ])
42
- unique_list.prepend(%w[ 1 2 3 4 ])
58
+ unique_list.append(%w[ 2 3 4 ]) # => LREM myuniquelist 0, "2" + LREM myuniquelist 0, "3" + LREM myuniquelist 0, "4" + RPUSH myuniquelist "2", "3", "4"
59
+ unique_list.prepend(%w[ 1 2 3 4 ]) # => LREM myuniquelist 0, "1" + LREM myuniquelist 0, "2" + LREM myuniquelist 0, "3" + LREM myuniquelist 0, "4" + LPUSH myuniquelist "1", "2", "3", "4"
43
60
  unique_list.append([])
44
- unique_list << "5"
45
- unique_list.remove(3)
46
- [ "1", "2", "4", "5" ] == unique_list.elements
61
+ unique_list << "5" # => LREM myuniquelist 0, "5" + RPUSH myuniquelist "5"
62
+ unique_list.remove(3) # => LREM myuniquelist 0, "3"
63
+ [ "4", "2", "1", "5" ] == unique_list.elements # => LRANGE myuniquelist 0, -1
47
64
 
48
65
  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
66
+ set.add(DateTime.tomorrow, DateTime.yesterday) # => SADD myset "2021-02-03 00:00:00 +0100" "2021-02-01 00:00:00 +0100"
67
+ set << DateTime.tomorrow # => SADD myset "2021-02-03 00:00:00 +0100"
68
+ 2 == set.size # => SCARD myset
69
+ [ DateTime.tomorrow, DateTime.yesterday ] == set.members # => SMEMBERS myset
70
+
71
+ hash = Kredis.hash "myhash"
72
+ hash.update("key" => "value", "key2" => "value2") # => HSET myhash "key", "value", "key2", "value2"
73
+ { "key" => "value", "key2" => "value2" } == hash.to_h # => HGETALL myhash
74
+ "value2" == hash["key2"] # => HMGET myhash "key2"
75
+ %w[ key key2 ] == hash.keys # => HKEYS myhash
76
+ %w[ value value2 ] == hash.values # => HVALS myhash
77
+ hash.remove # => DEL myhash
78
+
79
+ high_scores = Kredis.hash "high_scores", typed: :integer
80
+ high_scores.update(space_invaders: 100, pong: 42) # HSET high_scores "space_invaders", "100", "pong", "42"
81
+ %w[ space_invaders pong ] == high_scores.keys # HKEYS high_scores
82
+ [ 100, 42 ] == high_scores.values # HVALS high_scores
83
+ { "space_invaders" => 100, "pong" => 42 } == high_scores.to_h # HGETALL high_scores
53
84
 
54
85
  head_count = Kredis.counter "headcount"
55
86
  0 == head_count.value # => GET "headcount"
56
- head_count.increment
57
- head_count.increment
58
- head_count.decrement
87
+ head_count.increment # => SET headcount 0 NX + INCRBY headcount 1
88
+ head_count.increment # => SET headcount 0 NX + INCRBY headcount 1
89
+ head_count.decrement # => SET headcount 0 NX + DECRBY headcount 1
59
90
  1 == head_count.value # => GET "headcount"
60
91
 
61
92
  counter = Kredis.counter "mycounter", expires_in: 5.seconds
62
- counter.increment by: 2 # => SETEX "mycounter" 900 0 + INCR "mycounter" 2
93
+ counter.increment by: 2 # => SET mycounter 0 EX 5 NX + INCRBY "mycounter" 2
63
94
  2 == counter.value # => GET "mycounter"
64
95
  sleep 6.seconds
65
96
  0 == counter.value # => GET "mycounter"
66
97
 
67
98
  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
99
+ :one == cycle.value # => GET mycycle
100
+ cycle.next # => GET mycycle + SET mycycle 1
101
+ :two == cycle.value # => GET mycycle
102
+ cycle.next # => GET mycycle + SET mycycle 2
103
+ :three == cycle.value # => GET mycycle
104
+ cycle.next # => GET mycycle + SET mycycle 0
105
+ :one == cycle.value # => GET mycycle
75
106
 
76
107
  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
108
+ "one" == enum.value # => GET myenum
109
+ true == enum.one? # => GET myenum
110
+ enum.value = "two" # => SET myenum "two"
111
+ "two" == enum.value # => GET myenum
81
112
  enum.value = "four"
82
- "two" == enum.value
83
- enum.reset
84
- "one" == enum.value
113
+ "two" == enum.value # => GET myenum
114
+ enum.reset # => DEL myenum
115
+ "one" == enum.value # => GET myenum
85
116
 
86
117
  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
118
+ true == slots.available? # => GET myslots
119
+ slots.reserve # => INCR myslots
120
+ true == slots.available? # => GET myslots
121
+ slots.reserve # => INCR myslots
122
+ true == slots.available? # => GET myslots
123
+ slots.reserve # => INCR myslots
124
+ false == slots.available? # => GET myslots
125
+ slots.reserve # => INCR myslots + DECR myslots
126
+ false == slots.available? # => GET myslots
127
+ slots.release # => DECR myslots
128
+ true == slots.available? # => GET myslots
129
+ slots.reset # => DEL myslots
130
+
131
+
132
+ slot = Kredis.slot "myslot"
133
+ true == slot.available? # => GET myslot
134
+ slot.reserve # => INCR myslot
135
+ false == slot.available? # => GET myslot
136
+ slot.release # => DECR myslot
137
+ true == slot.available? # => GET myslot
138
+ slot.reset # => DEL myslot
99
139
 
100
140
  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?
141
+ false == flag.marked? # => EXISTS myflag
142
+ flag.mark # => SET myflag 1
143
+ true == flag.marked? # => EXISTS myflag
144
+ flag.remove # => DEL myflag
145
+ false == flag.marked? # => EXISTS myflag
146
+
147
+ true == flag.mark(expires_in: 1.second, force: false) #=> SET myflag 1 EX 1 NX
148
+ false == flag.mark(expires_in: 10.seconds, force: false) #=> SET myflag 10 EX 1 NX
149
+ true == flag.marked? #=> EXISTS myflag
109
150
  sleep 0.5.seconds
110
- true == flag.marked?
151
+ true == flag.marked? #=> EXISTS myflag
111
152
  sleep 0.6.seconds
112
- false == flag.marked?
153
+ false == flag.marked? #=> EXISTS myflag
113
154
  ```
114
155
 
115
156
  And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`:
@@ -133,12 +174,23 @@ class Person < ApplicationRecord
133
174
  end
134
175
 
135
176
  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?
177
+ person.names.append "David", "Heinemeier", "Hansson" # => RPUSH people:5:names "David" "Heinemeier" "Hansson"
178
+ true == person.morning.bright? # => GET people:5:morning
179
+ person.morning.value = "blue" # => SET people:5:morning
180
+ true == person.morning.blue? # => GET people:5:morning
140
181
  ```
141
182
 
183
+ You can also define `after_change` callbacks that trigger on mutations:
184
+
185
+ ```ruby
186
+ class Person < ApplicationRecord
187
+ kredis_list :names, after_change: ->(p) { }
188
+ kredis_unique_list :skills, limit: 2, after_change: :skillset_changed
189
+
190
+ def skillset_changed
191
+ end
192
+ end
193
+ ```
142
194
 
143
195
  ## Installation
144
196
 
@@ -165,6 +217,42 @@ test:
165
217
 
166
218
  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`.
167
219
 
220
+ ### Setting SSL options on Redis Connections
221
+
222
+ If you need to connect to Redis with SSL, the recommended approach is to set your Redis instance manually by adding an entry to the `Kredis::Connections.connections` hash. Below an example showing how to connect to Redis using Client Authentication:
223
+
224
+ ```ruby
225
+ Kredis::Connections.connections[:shared] = Redis.new(
226
+ url: ENV['REDIS_URL'],
227
+ ssl_params: {
228
+ cert_store: OpenSSL::X509::Store.new.tap { |store|
229
+ store.add_file(Rails.root.join('config', 'ca_cert.pem').to_s)
230
+ },
231
+
232
+ cert: OpenSSL::X509::Certificate.new(File.read(
233
+ Rails.root.join('config', 'client.crt')
234
+ )),
235
+
236
+ key: OpenSSL::PKey::RSA.new(
237
+ Rails.application.credentials.redis[:client_key]
238
+ ),
239
+
240
+ verify_mode: OpenSSL::SSL::VERIFY_PEER
241
+ }
242
+ )
243
+ ```
244
+
245
+ The above code could be added to either `config/environments/production.rb` or an initializer. Please ensure that your client private key, if used, is stored your credentials file or another secure location.
246
+
247
+ ### Configure how the redis client is created
248
+
249
+ You can configure how the redis client is created by setting `config.connector` in your `application.rb`:
250
+
251
+ ```ruby
252
+ config.kredis.connector = ->(config) { SomeRedisProxy.new(config) }
253
+ ```
254
+
255
+ By default Kredis will use `Redis.new(config)`.
168
256
 
169
257
  ## License
170
258
 
@@ -2,68 +2,87 @@ module Kredis::Attributes
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  class_methods do
5
- def kredis_proxy(name, key: nil, config: :shared)
6
- kredis_connection_with __method__, name, key, config: config
5
+ def kredis_proxy(name, key: nil, config: :shared, after_change: nil)
6
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change
7
7
  end
8
8
 
9
- def kredis_string(name, key: nil, config: :shared)
10
- kredis_connection_with __method__, name, key, config: config
9
+ def kredis_string(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
10
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
11
11
  end
12
12
 
13
- def kredis_integer(name, key: nil, config: :shared)
14
- kredis_connection_with __method__, name, key, config: config
13
+ def kredis_integer(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
14
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
15
15
  end
16
16
 
17
- def kredis_datetime(name, key: nil, config: :shared)
18
- kredis_connection_with __method__, name, key, config: config
17
+ def kredis_decimal(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
18
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
19
19
  end
20
20
 
21
- def kredis_flag(name, key: nil, config: :shared)
22
- kredis_connection_with __method__, name, key, config: config
21
+ def kredis_datetime(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
22
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
23
+ end
24
+
25
+ def kredis_flag(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
26
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
23
27
 
24
28
  define_method("#{name}?") do
25
29
  send(name).marked?
26
30
  end
27
31
  end
28
32
 
29
- def kredis_enum(name, key: nil, values:, default:, config: :shared)
30
- kredis_connection_with __method__, name, key, values: values, default: default, config: config
33
+ def kredis_float(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
34
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
35
+ end
36
+
37
+ def kredis_enum(name, key: nil, values:, default:, config: :shared, after_change: nil)
38
+ kredis_connection_with __method__, name, key, values: values, default: default, config: config, after_change: after_change
39
+ end
40
+
41
+ def kredis_json(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
42
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
31
43
  end
32
44
 
33
- def kredis_json(name, key: nil, config: :shared)
34
- kredis_connection_with __method__, name, key, config: config
45
+ def kredis_list(name, key: nil, typed: :string, config: :shared, after_change: nil)
46
+ kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change
35
47
  end
36
48
 
37
- def kredis_list(name, key: nil, typed: :string, config: :shared)
38
- kredis_connection_with __method__, name, key, typed: typed, config: config
49
+ def kredis_unique_list(name, limit: nil, key: nil, typed: :string, config: :shared, after_change: nil)
50
+ kredis_connection_with __method__, name, key, limit: limit, typed: typed, config: config, after_change: after_change
39
51
  end
40
52
 
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
53
+ def kredis_set(name, key: nil, typed: :string, config: :shared, after_change: nil)
54
+ kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change
43
55
  end
44
56
 
45
- def kredis_set(name, key: nil, typed: :string, config: :shared)
46
- kredis_connection_with __method__, name, key, typed: typed, config: config
57
+ def kredis_slot(name, key: nil, config: :shared, after_change: nil)
58
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change
47
59
  end
48
60
 
49
- def kredis_slot(name, key: nil, config: :shared)
50
- kredis_connection_with __method__, name, key, config: config
61
+ def kredis_slots(name, available:, key: nil, config: :shared, after_change: nil)
62
+ kredis_connection_with __method__, name, key, available: available, config: config, after_change: after_change
51
63
  end
52
64
 
53
- def kredis_slots(name, available:, key: nil, config: :shared)
54
- kredis_connection_with __method__, name, key, available: available, config: config
65
+ def kredis_counter(name, key: nil, config: :shared, after_change: nil)
66
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change
67
+ end
68
+
69
+ def kredis_hash(name, key: nil, typed: :string, config: :shared, after_change: nil)
70
+ kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change
55
71
  end
56
72
 
57
73
  private
58
74
  def kredis_connection_with(method, name, key, **options)
59
75
  ivar_symbol = :"@#{name}_#{method}"
60
76
  type = method.to_s.sub("kredis_", "")
77
+ after_change = options.delete(:after_change)
61
78
 
62
79
  define_method(name) do
63
80
  if instance_variable_defined?(ivar_symbol)
64
81
  instance_variable_get(ivar_symbol)
65
82
  else
66
- instance_variable_set(ivar_symbol, Kredis.send(type, kredis_key_evaluated(key) || kredis_key_for_attribute(name), **options))
83
+ new_type = Kredis.send(type, kredis_key_evaluated(key) || kredis_key_for_attribute(name), **options)
84
+ instance_variable_set ivar_symbol,
85
+ after_change ? enrich_after_change_with_record_access(new_type, after_change) : new_type
67
86
  end
68
87
  end
69
88
  end
@@ -84,4 +103,11 @@ module Kredis::Attributes
84
103
  def extract_kredis_id
85
104
  try(:id) or raise NotImplementedError, "kredis needs a unique id, either implement an id method or pass a custom key."
86
105
  end
106
+
107
+ def enrich_after_change_with_record_access(type, original_after_change)
108
+ case original_after_change
109
+ when Proc then Kredis::Types::CallbacksProxy.new(type, ->(_) { original_after_change.call(self) })
110
+ when Symbol then Kredis::Types::CallbacksProxy.new(type, ->(_) { send(original_after_change) })
111
+ end
112
+ end
87
113
  end
@@ -3,22 +3,25 @@ require "redis"
3
3
  module Kredis::Connections
4
4
  mattr_accessor :connections, default: Hash.new
5
5
  mattr_accessor :configurator
6
+ mattr_accessor :connector, default: ->(config) { Redis.new(config) }
6
7
 
7
8
  def configured_for(name)
8
9
  connections[name] ||= begin
9
- logger&.info "[Kredis] Connected to #{name}"
10
- Redis.new configurator.config_for("redis/#{name}")
10
+ Kredis.instrument :meta, message: "Connected to #{name}" do
11
+ connector.call configurator.config_for("redis/#{name}")
12
+ end
11
13
  end
12
14
  end
13
15
 
14
16
  def clear_all
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
17
+ Kredis.instrument :meta, message: "Connections all cleared" do
18
+ connections.each_value do |connection|
19
+ if Kredis.namespace
20
+ keys = connection.keys("#{Kredis.namespace}:*")
21
+ connection.del keys if keys.any?
22
+ else
23
+ connection.flushdb
24
+ end
22
25
  end
23
26
  end
24
27
  end
@@ -0,0 +1,22 @@
1
+ require "active_support/log_subscriber"
2
+
3
+ class Kredis::LogSubscriber < ActiveSupport::LogSubscriber
4
+ def proxy(event)
5
+ debug formatted_in(YELLOW, event, type: "Proxy")
6
+ end
7
+
8
+ def migration(event)
9
+ debug formatted_in(YELLOW, event, type: "Migration")
10
+ end
11
+
12
+ def meta(event)
13
+ info formatted_in(MAGENTA, event)
14
+ end
15
+
16
+ private
17
+ def formatted_in(color, event, type: nil)
18
+ color " Kredis #{type} (#{event.duration.round(1)}ms) #{event.payload[:message]}", color, true
19
+ end
20
+ end
21
+
22
+ Kredis::LogSubscriber.attach_to :kredis
@@ -22,8 +22,9 @@ class Kredis::Migration
22
22
  namespaced_to = Kredis.namespaced_key(to)
23
23
 
24
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 ]
25
+ log_migration "Migrating key #{from} to #{namespaced_to}" do
26
+ @redis.evalsha @copy_sha, keys: [ from, namespaced_to ]
27
+ end
27
28
  else
28
29
  log_migration "Skipping blank/unaltered migration key #{from} → #{to}"
29
30
  end
@@ -46,7 +47,7 @@ class Kredis::Migration
46
47
  end until cursor == "0"
47
48
  end
48
49
 
49
- def log_migration(message)
50
- Kredis.logger&.debug "[Kredis Migration] #{message}"
50
+ def log_migration(message, &block)
51
+ Kredis.instrument :migration, message: message, &block
51
52
  end
52
53
  end
@@ -9,7 +9,11 @@ class Kredis::Railtie < ::Rails::Railtie
9
9
  end
10
10
 
11
11
  initializer "kredis.logger" do
12
- Kredis.logger = config.kredis.logger || Rails.logger
12
+ Kredis::LogSubscriber.logger = config.kredis.logger || Rails.logger
13
+ end
14
+
15
+ initializer "kredis.configuration" do
16
+ Kredis::Connections.connector = config.kredis.connector || ->(config) { Redis.new(config) }
13
17
  end
14
18
 
15
19
  initializer "kredis.configurator" do
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kredis
4
+ module Type
5
+ class DateTime < ActiveModel::Type::DateTime
6
+ def serialize(value)
7
+ super&.iso8601(9)
8
+ end
9
+
10
+ def cast_value(value)
11
+ super&.to_datetime
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kredis
4
+ module Type
5
+ class Json < ActiveModel::Type::Value
6
+ def type
7
+ :json
8
+ end
9
+
10
+ def cast_value(value)
11
+ JSON.load(value)
12
+ end
13
+
14
+ def serialize(value)
15
+ JSON.dump(value)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,45 +1,35 @@
1
+ require "json"
2
+ require "active_model/type"
3
+ require "kredis/type/json"
4
+ require "kredis/type/datetime"
5
+
1
6
  module Kredis::TypeCasting
2
7
  class InvalidType < StandardError; end
3
8
 
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
9
+ TYPES = {
10
+ string: ActiveModel::Type::String.new,
11
+ integer: ActiveModel::Type::Integer.new,
12
+ decimal: ActiveModel::Type::Decimal.new,
13
+ float: ActiveModel::Type::Float.new,
14
+ boolean: ActiveModel::Type::Boolean.new,
15
+ datetime: Kredis::Type::DateTime.new,
16
+ json: Kredis::Type::Json.new
17
+ }
18
+
19
+ def type_to_string(value, type)
20
+ raise InvalidType if type && !TYPES.key?(type)
21
+
22
+ TYPES[type || :string].serialize(value)
25
23
  end
26
24
 
27
25
  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?
26
+ raise InvalidType if type && !TYPES.key?(type)
27
+
28
+ TYPES[type || :string].cast(value)
39
29
  end
40
30
 
41
- def types_to_strings(values)
42
- Array(values).flatten.map { |value| type_to_string(value) }
31
+ def types_to_strings(values, type)
32
+ Array(values).flatten.map { |value| type_to_string(value, type) }
43
33
  end
44
34
 
45
35
  def strings_to_types(values, type)
@@ -0,0 +1,31 @@
1
+ class Kredis::Types::CallbacksProxy
2
+ attr_reader :type
3
+ delegate :to_s, to: :type
4
+
5
+ AFTER_CHANGE_OPERATIONS = {
6
+ Kredis::Types::Counter => %i[ increment decrement reset ],
7
+ Kredis::Types::Cycle => %i[ next ],
8
+ Kredis::Types::Enum => %i[ value= reset ],
9
+ Kredis::Types::Flag => %i[ mark remove ],
10
+ Kredis::Types::Hash => %i[ update delete ],
11
+ Kredis::Types::List => %i[ remove prepend append << ],
12
+ Kredis::Types::Scalar => %i[ value= clear ],
13
+ Kredis::Types::Set => %i[ add << remove replace take clear ],
14
+ Kredis::Types::Slots => %i[ reserve release reset ]
15
+ }
16
+
17
+ def initialize(type, callback)
18
+ @type, @callback = type, callback
19
+ end
20
+
21
+ def method_missing(method, *args, **kwargs, &block)
22
+ result = type.send(method, *args, **kwargs, &block)
23
+ invoke_suitable_after_change_callback_for method
24
+ result
25
+ end
26
+
27
+ private
28
+ def invoke_suitable_after_change_callback_for(method)
29
+ @callback.call(type) if AFTER_CHANGE_OPERATIONS[type.class]&.include? method
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  class Kredis::Types::Counter < Kredis::Types::Proxying
2
- proxying :multi, :set, :incrby, :decrby, :get, :del
2
+ proxying :multi, :set, :incrby, :decrby, :get, :del, :exists?
3
3
 
4
4
  attr_accessor :expires_in
5
5
 
@@ -1,5 +1,7 @@
1
+ require "active_support/core_ext/object/inclusion"
2
+
1
3
  class Kredis::Types::Enum < Kredis::Types::Proxying
2
- proxying :set, :get, :del
4
+ proxying :set, :get, :del, :exists?
3
5
 
4
6
  attr_accessor :values, :default
5
7
 
@@ -1,8 +1,10 @@
1
1
  class Kredis::Types::Flag < Kredis::Types::Proxying
2
2
  proxying :set, :exists?, :del
3
3
 
4
- def mark(expires_in: nil)
5
- set 1, ex: expires_in
4
+ attr_accessor :expires_in
5
+
6
+ def mark(expires_in: nil, force: true)
7
+ set 1, ex: expires_in || self.expires_in, nx: !force
6
8
  end
7
9
 
8
10
  def marked?
@@ -0,0 +1,44 @@
1
+ require "active_support/core_ext/hash"
2
+
3
+ class Kredis::Types::Hash < Kredis::Types::Proxying
4
+ proxying :hget, :hset, :hmget, :hdel, :hgetall, :hkeys, :hvals, :del, :exists?
5
+
6
+ attr_accessor :typed
7
+
8
+ def [](key)
9
+ string_to_type(hget(key), typed)
10
+ end
11
+
12
+ def []=(key, value)
13
+ update key => value
14
+ end
15
+
16
+ def update(**entries)
17
+ hset entries.transform_values{ |val| type_to_string(val, typed) } if entries.flatten.any?
18
+ end
19
+
20
+ def values_at(*keys)
21
+ strings_to_types(hmget(keys) || [], typed)
22
+ end
23
+
24
+ def delete(*keys)
25
+ hdel keys if keys.flatten.any?
26
+ end
27
+
28
+ def remove
29
+ del
30
+ end
31
+
32
+ def entries
33
+ (hgetall || {}).transform_values { |val| string_to_type(val, typed) }.with_indifferent_access
34
+ end
35
+ alias to_h entries
36
+
37
+ def keys
38
+ hkeys || []
39
+ end
40
+
41
+ def values
42
+ strings_to_types(hvals || [], typed)
43
+ end
44
+ end
@@ -1,5 +1,5 @@
1
1
  class Kredis::Types::List < Kredis::Types::Proxying
2
- proxying :lrange, :lrem, :lpush, :rpush
2
+ proxying :lrange, :lrem, :lpush, :rpush, :exists?
3
3
 
4
4
  attr_accessor :typed
5
5
 
@@ -9,15 +9,15 @@ class Kredis::Types::List < Kredis::Types::Proxying
9
9
  alias to_a elements
10
10
 
11
11
  def remove(*elements)
12
- types_to_strings(elements).each { |element| lrem 0, element }
12
+ types_to_strings(elements, typed).each { |element| lrem 0, element }
13
13
  end
14
14
 
15
15
  def prepend(*elements)
16
- lpush types_to_strings(elements) if elements.flatten.any?
16
+ lpush types_to_strings(elements, typed) if elements.flatten.any?
17
17
  end
18
18
 
19
19
  def append(*elements)
20
- rpush types_to_strings(elements) if elements.flatten.any?
20
+ rpush types_to_strings(elements, typed) if elements.flatten.any?
21
21
  end
22
22
  alias << append
23
23
  end
@@ -14,9 +14,10 @@ class Kredis::Types::Proxy
14
14
  end
15
15
 
16
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
17
+ Kredis.instrument :proxy, **log_message(method, *args, **kwargs) do
18
+ failsafe do
19
+ redis.public_send method, key, *args, **kwargs
20
+ end
20
21
  end
21
22
  end
22
23
 
@@ -24,8 +25,7 @@ class Kredis::Types::Proxy
24
25
  def log_message(method, *args, **kwargs)
25
26
  args = args.flatten.reject(&:blank?).presence
26
27
  kwargs = kwargs.reject { |_k, v| v.blank? }.presence
27
- type_name = self.class.name.split("::").last
28
28
 
29
- "[Kredis #{type_name}] #{method.upcase} #{key} #{args&.inspect} #{kwargs&.inspect}".chomp
29
+ { message: "#{method.upcase} #{key} #{args&.inspect} #{kwargs&.inspect}".chomp }
30
30
  end
31
31
  end
@@ -1,14 +1,20 @@
1
1
  class Kredis::Types::Scalar < Kredis::Types::Proxying
2
- proxying :set, :get, :exists?, :del
2
+ proxying :set, :get, :exists?, :del, :expire, :expireat
3
3
 
4
- attr_accessor :typed, :default
4
+ attr_accessor :typed, :default, :expires_in
5
5
 
6
6
  def value=(value)
7
- set type_to_string(value)
7
+ set type_to_string(value, typed), ex: expires_in
8
8
  end
9
9
 
10
10
  def value
11
- string_to_type(get, typed) || default
11
+ value_after_casting = string_to_type(get, typed)
12
+
13
+ if value_after_casting.nil?
14
+ default
15
+ else
16
+ value_after_casting
17
+ end
12
18
  end
13
19
 
14
20
  def to_s
@@ -22,4 +28,12 @@ class Kredis::Types::Scalar < Kredis::Types::Proxying
22
28
  def clear
23
29
  del
24
30
  end
31
+
32
+ def expire_in(seconds)
33
+ expire seconds.to_i
34
+ end
35
+
36
+ def expire_at(datetime)
37
+ expireat datetime.to_i
38
+ end
25
39
  end
@@ -1,5 +1,5 @@
1
1
  class Kredis::Types::Set < Kredis::Types::Proxying
2
- proxying :smembers, :sadd, :srem, :multi, :del, :sismember, :scard, :spop
2
+ proxying :smembers, :sadd, :srem, :multi, :del, :sismember, :scard, :spop, :exists?
3
3
 
4
4
  attr_accessor :typed
5
5
 
@@ -9,12 +9,12 @@ class Kredis::Types::Set < Kredis::Types::Proxying
9
9
  alias to_a members
10
10
 
11
11
  def add(*members)
12
- sadd types_to_strings(members) if members.flatten.any?
12
+ sadd types_to_strings(members, typed) if members.flatten.any?
13
13
  end
14
14
  alias << add
15
15
 
16
16
  def remove(*members)
17
- srem types_to_strings(members) if members.flatten.any?
17
+ srem types_to_strings(members, typed) if members.flatten.any?
18
18
  end
19
19
 
20
20
  def replace(*members)
@@ -25,7 +25,7 @@ class Kredis::Types::Set < Kredis::Types::Proxying
25
25
  end
26
26
 
27
27
  def include?(member)
28
- sismember type_to_string(member)
28
+ sismember type_to_string(member, typed)
29
29
  end
30
30
 
31
31
  def size
@@ -1,7 +1,7 @@
1
1
  class Kredis::Types::Slots < Kredis::Types::Proxying
2
2
  class NotAvailable < StandardError; end
3
3
 
4
- proxying :incr, :decr, :get, :del
4
+ proxying :incr, :decr, :get, :del, :exists?
5
5
 
6
6
  attr_accessor :available
7
7
 
@@ -1,6 +1,6 @@
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
- proxying :multi, :ltrim
3
+ proxying :multi, :ltrim, :exists?
4
4
 
5
5
  attr_accessor :typed, :limit
6
6
 
data/lib/kredis/types.rb CHANGED
@@ -1,77 +1,90 @@
1
1
  module Kredis::Types
2
- def proxy(key, config: :shared)
3
- Proxy.new configured_for(config), namespaced_key(key)
2
+ autoload :CallbacksProxy, "kredis/types/callbacks_proxy"
3
+
4
+ def proxy(key, config: :shared, after_change: nil)
5
+ type_from(Proxy, config, key, after_change: after_change)
4
6
  end
5
7
 
6
8
 
7
- def scalar(key, typed: :string, default: nil, config: :shared)
8
- Scalar.new configured_for(config), namespaced_key(key), typed: typed, default: default
9
+ def scalar(key, typed: :string, default: nil, config: :shared, after_change: nil, expires_in: nil)
10
+ type_from(Scalar, config, key, after_change: after_change, typed: typed, default: default, expires_in: expires_in)
9
11
  end
10
12
 
11
- def string(key, default: nil, config: :shared)
12
- Scalar.new configured_for(config), namespaced_key(key), typed: :string, default: default
13
+ def string(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
14
+ type_from(Scalar, config, key, after_change: after_change, typed: :string, default: default, expires_in: expires_in)
13
15
  end
14
16
 
15
- def integer(key, default: nil, config: :shared)
16
- Scalar.new configured_for(config), namespaced_key(key), typed: :integer, default: default
17
+ def integer(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
18
+ type_from(Scalar, config, key, after_change: after_change, typed: :integer, default: default, expires_in: expires_in)
17
19
  end
18
20
 
19
- def decimal(key, default: nil, config: :shared)
20
- Scalar.new configured_for(config), namespaced_key(key), typed: :decimal, default: default
21
+ def decimal(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
22
+ type_from(Scalar, config, key, after_change: after_change, typed: :decimal, default: default, expires_in: expires_in)
21
23
  end
22
24
 
23
- def float(key, default: nil, config: :shared)
24
- Scalar.new configured_for(config), namespaced_key(key), typed: :float, default: default
25
+ def float(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
26
+ type_from(Scalar, config, key, after_change: after_change, typed: :float, default: default, expires_in: expires_in)
25
27
  end
26
28
 
27
- def boolean(key, default: nil, config: :shared)
28
- Scalar.new configured_for(config), namespaced_key(key), typed: :boolean, default: default
29
+ def boolean(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
30
+ type_from(Scalar, config, key, after_change: after_change, typed: :boolean, default: default, expires_in: expires_in)
29
31
  end
30
32
 
31
- def datetime(key, default: nil, config: :shared)
32
- Scalar.new configured_for(config), namespaced_key(key), typed: :datetime, default: default
33
+ def datetime(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
34
+ type_from(Scalar, config, key, after_change: after_change, typed: :datetime, default: default, expires_in: expires_in)
33
35
  end
34
36
 
35
- def json(key, default: nil, config: :shared)
36
- Scalar.new configured_for(config), namespaced_key(key), typed: :json, default: default
37
+ def json(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
38
+ type_from(Scalar, config, key, after_change: after_change, typed: :json, default: default, expires_in: expires_in)
37
39
  end
38
40
 
39
41
 
40
- def counter(key, expires_in: nil, config: :shared)
41
- Counter.new configured_for(config), namespaced_key(key), expires_in: expires_in
42
+ def counter(key, expires_in: nil, config: :shared, after_change: nil)
43
+ type_from(Counter, config, key, after_change: after_change, expires_in: expires_in)
44
+ end
45
+
46
+ def cycle(key, values:, expires_in: nil, config: :shared, after_change: nil)
47
+ type_from(Cycle, config, key, after_change: after_change, values: values, expires_in: expires_in)
42
48
  end
43
49
 
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
50
+ def flag(key, config: :shared, after_change: nil, expires_in: nil)
51
+ type_from(Flag, config, key, after_change: after_change, expires_in: expires_in)
46
52
  end
47
53
 
48
- def flag(key, config: :shared)
49
- Flag.new configured_for(config), namespaced_key(key)
54
+ def enum(key, values:, default:, config: :shared, after_change: nil)
55
+ type_from(Enum, config, key, after_change: after_change, values: values, default: default)
50
56
  end
51
57
 
52
- def enum(key, values:, default:, config: :shared)
53
- Enum.new configured_for(config), namespaced_key(key), values: values, default: default
58
+ def hash(key, typed: :string, config: :shared, after_change: nil)
59
+ type_from(Hash, config, key, after_change: after_change, typed: typed)
54
60
  end
55
61
 
56
- def list(key, typed: :string, config: :shared)
57
- List.new configured_for(config), namespaced_key(key), typed: typed
62
+ def list(key, typed: :string, config: :shared, after_change: nil)
63
+ type_from(List, config, key, after_change: after_change, typed: typed)
58
64
  end
59
65
 
60
- def unique_list(key, typed: :string, limit: nil, config: :shared)
61
- UniqueList.new configured_for(config), namespaced_key(key), typed: typed, limit: limit
66
+ def unique_list(key, typed: :string, limit: nil, config: :shared, after_change: nil)
67
+ type_from(UniqueList, config, key, after_change: after_change, typed: typed, limit: limit)
62
68
  end
63
69
 
64
- def set(key, typed: :string, config: :shared)
65
- Set.new configured_for(config), namespaced_key(key), typed: typed
70
+ def set(key, typed: :string, config: :shared, after_change: nil)
71
+ type_from(Set, config, key, after_change: after_change, typed: typed)
66
72
  end
67
73
 
68
- def slot(key, config: :shared)
69
- Slots.new configured_for(config), namespaced_key(key), available: 1
74
+ def slot(key, config: :shared, after_change: nil)
75
+ type_from(Slots, config, key, after_change: after_change, available: 1)
70
76
  end
71
77
 
72
- def slots(key, available:, config: :shared)
73
- Slots.new configured_for(config), namespaced_key(key), available: available
78
+ def slots(key, available:, config: :shared, after_change: nil)
79
+ type_from(Slots, config, key, after_change: after_change, available: available)
74
80
  end
81
+
82
+ private
83
+ def type_from(type_klass, config, key, after_change: nil, **options)
84
+ type_klass.new(configured_for(config), namespaced_key(key), **options).then do |type|
85
+ after_change ? CallbacksProxy.new(type, after_change) : type
86
+ end
87
+ end
75
88
  end
76
89
 
77
90
  require "kredis/types/proxy"
@@ -82,6 +95,7 @@ require "kredis/types/counter"
82
95
  require "kredis/types/cycle"
83
96
  require "kredis/types/flag"
84
97
  require "kredis/types/enum"
98
+ require "kredis/types/hash"
85
99
  require "kredis/types/list"
86
100
  require "kredis/types/unique_list"
87
101
  require "kredis/types/set"
@@ -1,3 +1,3 @@
1
1
  module Kredis
2
- VERSION = "0.2.3"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/kredis.rb CHANGED
@@ -4,6 +4,7 @@ require "active_support/core_ext/module/attribute_accessors"
4
4
  require "kredis/version"
5
5
 
6
6
  require "kredis/connections"
7
+ require "kredis/log_subscriber"
7
8
  require "kredis/namespace"
8
9
  require "kredis/type_casting"
9
10
  require "kredis/types"
@@ -13,12 +14,17 @@ require "kredis/railtie" if defined?(Rails::Railtie)
13
14
 
14
15
  module Kredis
15
16
  include Connections, Namespace, TypeCasting, Types
16
-
17
17
  extend self
18
18
 
19
+ autoload :Migration, "kredis/migration"
20
+
19
21
  mattr_accessor :logger
20
22
 
21
23
  def redis(config: :shared)
22
24
  configured_for(config)
23
25
  end
26
+
27
+ def instrument(channel, **options, &block)
28
+ ActiveSupport::Notifications.instrument("#{channel}.kredis", **options, &block)
29
+ end
24
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kredis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasper Timm Hansen
@@ -9,10 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-02-11 00:00:00.000000000 Z
12
+ date: 2021-11-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: rails
15
+ name: activesupport
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
18
  - - ">="
@@ -39,6 +39,20 @@ dependencies:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '4.2'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rails
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 6.0.0
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 6.0.0
42
56
  description:
43
57
  email: david@hey.com
44
58
  executables: []
@@ -50,15 +64,20 @@ files:
50
64
  - lib/kredis.rb
51
65
  - lib/kredis/attributes.rb
52
66
  - lib/kredis/connections.rb
67
+ - lib/kredis/log_subscriber.rb
53
68
  - lib/kredis/migration.rb
54
69
  - lib/kredis/namespace.rb
55
70
  - lib/kredis/railtie.rb
71
+ - lib/kredis/type/datetime.rb
72
+ - lib/kredis/type/json.rb
56
73
  - lib/kredis/type_casting.rb
57
74
  - lib/kredis/types.rb
75
+ - lib/kredis/types/callbacks_proxy.rb
58
76
  - lib/kredis/types/counter.rb
59
77
  - lib/kredis/types/cycle.rb
60
78
  - lib/kredis/types/enum.rb
61
79
  - lib/kredis/types/flag.rb
80
+ - lib/kredis/types/hash.rb
62
81
  - lib/kredis/types/list.rb
63
82
  - lib/kredis/types/proxy.rb
64
83
  - lib/kredis/types/proxy/failsafe.rb
@@ -87,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
106
  - !ruby/object:Gem::Version
88
107
  version: '0'
89
108
  requirements: []
90
- rubygems_version: 3.2.5
109
+ rubygems_version: 3.2.22
91
110
  signing_key:
92
111
  specification_version: 4
93
112
  summary: Higher-level data structures built on Redis.