kredis 0.2.2 → 0.4.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: f3e3619ec3a977b472348572202b324c813d550d43cc866ec1aab7dad8e463f0
4
- data.tar.gz: 920c49f0cfe443a71dd5dc8f112070ab8061bbe2d352c98b45026ceeed871f2e
3
+ metadata.gz: 0c7614f548dde6852fd7e654215ba56f103fec5ac7ad6ae477fe2621f3b9964a
4
+ data.tar.gz: a023026462ec8a09f1e26550ce56abaeb95b1f0582665787cda48ef91e9048d1
5
5
  SHA512:
6
- metadata.gz: 8f97bda6b251a100411c0338d2760aa1c2565f3ee1901f5f4971123cbe8b7a49fabf64ccc210775e9694263fd1a2a142641bcf81659776f746d05163640afda5
7
- data.tar.gz: 546b9246e3952a92452fe55cf574233b12d50f29bdeb345c7efe3ebf86bd3ab9a6ff4d22731438ae3258586caca530c1e7229ffee9b581a4a22b04f585dd4467
6
+ metadata.gz: e3755bc5be9370676ac5b2a4a6100a26d0ed09b1248d87114911020ec1b9eb6ab094d25ec6686478069add02197d5188f59edcab04ccc69fed9bb80d1db9966f
7
+ data.tar.gz: 6f97f9ad814700b69b5663697add1f4b2d3468d05752c053c883ca0e8d93703b91af06f6a2593ee6ba79835435663c51c78cd17df32dafb1d7efc666150e77f6
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,78 +46,110 @@ 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
 
98
+ cycle = Kredis.cycle "mycycle", values: %i[ one two three ]
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
106
+
67
107
  enum = Kredis.enum "myenum", values: %w[ one two three ], default: "one"
68
- "one" == enum.value
69
- true == enum.one?
70
- enum.value = "two"
71
- "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
72
112
  enum.value = "four"
73
- "two" == enum.value
74
- enum.reset
75
- "one" == enum.value
113
+ "two" == enum.value # => GET myenum
114
+ enum.reset # => DEL myenum
115
+ "one" == enum.value # => GET myenum
76
116
 
77
117
  slots = Kredis.slots "myslots", available: 3
78
- true == slots.available?
79
- slots.reserve
80
- true == slots.available?
81
- slots.reserve
82
- true == slots.available?
83
- slots.reserve
84
- true == slots.available?
85
- slots.reserve
86
- false == slots.available?
87
- slots.release
88
- true == slots.available?
89
- 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
90
139
 
91
140
  flag = Kredis.flag "myflag"
92
- false == flag.marked?
93
- flag.mark
94
- true == flag.marked?
95
- flag.remove
96
- false == flag.marked?
97
-
98
- flag.mark(expires_in: 1.second)
99
- 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
+ flag.mark(expires_in: 1.second) #=> SET myflag 1 EX 1
148
+ true == flag.marked? #=> EXISTS myflag
100
149
  sleep 0.5.seconds
101
- true == flag.marked?
150
+ true == flag.marked? #=> EXISTS myflag
102
151
  sleep 0.6.seconds
103
- false == flag.marked?
152
+ false == flag.marked? #=> EXISTS myflag
104
153
  ```
105
154
 
106
155
  And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`:
@@ -124,12 +173,23 @@ class Person < ApplicationRecord
124
173
  end
125
174
 
126
175
  person = Person.find(5)
127
- person.names.append "David", "Heinemeier", "Hansson" # => SADD person:5:names "David" "Heinemeier" "Hansson"
128
- true == person.morning.bright?
129
- person.morning.value = "blue"
130
- true == person.morning.blue?
176
+ person.names.append "David", "Heinemeier", "Hansson" # => RPUSH people:5:names "David" "Heinemeier" "Hansson"
177
+ true == person.morning.bright? # => GET people:1:morning
178
+ person.morning.value = "blue" # => SET people:1:morning
179
+ true == person.morning.blue? # => GET people:1:morning
131
180
  ```
132
181
 
182
+ You can also define `after_change` callbacks that trigger on mutations:
183
+
184
+ ```ruby
185
+ class Person < ApplicationRecord
186
+ kredis_list :names, after_change: ->(p) { }
187
+ kredis_unique_list :skills, limit: 2, after_change: :skillset_changed
188
+
189
+ def skillset_changed
190
+ end
191
+ end
192
+ ```
133
193
 
134
194
  ## Installation
135
195
 
@@ -156,6 +216,32 @@ test:
156
216
 
157
217
  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`.
158
218
 
219
+ ### Setting SSL options on Redis Connections
220
+
221
+ 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:
222
+
223
+ ```ruby
224
+ Kredis::Connections.connections[:shared] = Redis.new(
225
+ url: ENV['REDIS_URL'],
226
+ ssl_params: {
227
+ cert_store: OpenSSL::X509::Store.new.tap { |store|
228
+ store.add_file(Rails.root.join('config', 'ca_cert.pem').to_s)
229
+ },
230
+
231
+ cert: OpenSSL::X509::Certificate.new(File.read(
232
+ Rails.root.join('config', 'client.crt')
233
+ )),
234
+
235
+ key: OpenSSL::PKey::RSA.new(
236
+ Rails.application.credentials.redis[:client_key]
237
+ ),
238
+
239
+ verify_mode: OpenSSL::SSL::VERIFY_PEER
240
+ }
241
+ )
242
+ ```
243
+
244
+ 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.
159
245
 
160
246
  ## License
161
247
 
@@ -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)
26
+ kredis_connection_with __method__, name, key, config: config, after_change: after_change
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
@@ -6,13 +6,22 @@ module Kredis::Connections
6
6
 
7
7
  def configured_for(name)
8
8
  connections[name] ||= begin
9
- logger&.info "[Kredis] Connected to #{name}"
10
- Redis.new configurator.config_for("redis/#{name}")
9
+ Kredis.instrument :meta, message: "Connected to #{name}" do
10
+ Redis.new configurator.config_for("redis/#{name}")
11
+ end
11
12
  end
12
13
  end
13
14
 
14
15
  def clear_all
15
- logger&.info "[Kredis] Connections all cleared"
16
- connections.each_value(&:flushdb)
16
+ Kredis.instrument :meta, message: "Connections all cleared" do
17
+ connections.each_value do |connection|
18
+ if Kredis.namespace
19
+ keys = connection.keys("#{Kredis.namespace}:*")
20
+ connection.del keys if keys.any?
21
+ else
22
+ connection.flushdb
23
+ end
24
+ end
25
+ end
17
26
  end
18
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
@@ -5,6 +5,7 @@ class Kredis::Migration
5
5
 
6
6
  def initialize(config = :shared)
7
7
  @redis = Kredis.configured_for config
8
+ # TODO: Replace script loading with `copy` command once Redis 6.2+ is the minimum supported version.
8
9
  @copy_sha = @redis.script "load", "redis.call('SETNX', KEYS[2], redis.call('GET', KEYS[1])); return 1;"
9
10
  end
10
11
 
@@ -21,8 +22,9 @@ class Kredis::Migration
21
22
  namespaced_to = Kredis.namespaced_key(to)
22
23
 
23
24
  if to.present? && from != namespaced_to
24
- log_migration "Migrating key #{from} to #{namespaced_to}"
25
- @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
26
28
  else
27
29
  log_migration "Skipping blank/unaltered migration key #{from} → #{to}"
28
30
  end
@@ -45,7 +47,7 @@ class Kredis::Migration
45
47
  end until cursor == "0"
46
48
  end
47
49
 
48
- def log_migration(message)
49
- Kredis.logger&.debug "[Kredis Migration] #{message}"
50
+ def log_migration(message, &block)
51
+ Kredis.instrument :migration, message: message, &block
50
52
  end
51
53
  end
@@ -1,33 +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
-
7
- initializer "kredis.testing" do
8
- ActiveSupport.on_load(:active_support_test_case) do
9
- parallelize_setup { |worker| Kredis.namespace = "test-#{worker}" }
10
- teardown { Kredis.clear_all }
11
- 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 }
12
8
  end
9
+ end
13
10
 
14
- initializer "kredis.logger" do
15
- Kredis.logger = config.kredis.logger || Rails.logger
16
- end
11
+ initializer "kredis.logger" do
12
+ Kredis::LogSubscriber.logger = config.kredis.logger || Rails.logger
13
+ end
17
14
 
18
- initializer "kredis.configurator" do
19
- Kredis.configurator = Rails.application
20
- end
15
+ initializer "kredis.configurator" do
16
+ Kredis.configurator = Rails.application
17
+ end
21
18
 
22
- initializer "kredis.attributes" do
23
- # No load hook for Active Model, just defer until after initialization.
24
- config.after_initialize do
25
- ActiveModel::Model.include Kredis::Attributes if defined?(ActiveModel::Model)
26
- end
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
27
24
 
28
- ActiveSupport.on_load(:active_record) do
29
- include Kredis::Attributes
30
- end
25
+ ActiveSupport.on_load(:active_record) do
26
+ include Kredis::Attributes
31
27
  end
32
28
  end
33
29
  end
@@ -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
2
+ proxying :multi, :set, :incrby, :decrby, :get, :del
3
3
 
4
4
  attr_accessor :expires_in
5
5
 
@@ -20,4 +20,8 @@ class Kredis::Types::Counter < Kredis::Types::Proxying
20
20
  def value
21
21
  get.to_i
22
22
  end
23
+
24
+ def reset
25
+ del
26
+ end
23
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
@@ -1,3 +1,5 @@
1
+ require "active_support/core_ext/object/inclusion"
2
+
1
3
  class Kredis::Types::Enum < Kredis::Types::Proxying
2
4
  proxying :set, :get, :del
3
5
 
@@ -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
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
@@ -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
@@ -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
data/lib/kredis/types.rb CHANGED
@@ -1,73 +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)
11
+ end
12
+
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)
9
15
  end
10
16
 
11
- def string(key, default: nil, config: :shared)
12
- Scalar.new configured_for(config), namespaced_key(key), typed: :string, 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)
13
19
  end
14
20
 
15
- def integer(key, default: nil, config: :shared)
16
- Scalar.new configured_for(config), namespaced_key(key), typed: :integer, 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)
17
23
  end
18
24
 
19
- def decimal(key, default: nil, config: :shared)
20
- Scalar.new configured_for(config), namespaced_key(key), typed: :decimal, 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)
21
27
  end
22
28
 
23
- def float(key, default: nil, config: :shared)
24
- Scalar.new configured_for(config), namespaced_key(key), typed: :float, 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)
25
31
  end
26
32
 
27
- def boolean(key, default: nil, config: :shared)
28
- Scalar.new configured_for(config), namespaced_key(key), typed: :boolean, 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)
29
35
  end
30
36
 
31
- def datetime(key, default: nil, config: :shared)
32
- Scalar.new configured_for(config), namespaced_key(key), typed: :datetime, 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)
33
39
  end
34
40
 
35
- def json(key, default: nil, config: :shared)
36
- Scalar.new configured_for(config), namespaced_key(key), typed: :json, default: default
41
+
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)
37
44
  end
38
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)
48
+ end
39
49
 
40
- def counter(key, expires_in: nil, config: :shared)
41
- Counter.new configured_for(config), namespaced_key(key), expires_in: expires_in
50
+ def flag(key, config: :shared, after_change: nil)
51
+ type_from(Flag, config, key, after_change: after_change)
42
52
  end
43
53
 
44
- def flag(key, config: :shared)
45
- 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)
46
56
  end
47
57
 
48
- def enum(key, values:, default:, config: :shared)
49
- 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)
50
60
  end
51
61
 
52
- def list(key, typed: :string, config: :shared)
53
- 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)
54
64
  end
55
65
 
56
- def unique_list(key, typed: :string, limit: nil, config: :shared)
57
- 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)
58
68
  end
59
69
 
60
- def set(key, typed: :string, config: :shared)
61
- 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)
62
72
  end
63
73
 
64
- def slot(key, config: :shared)
65
- 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)
66
76
  end
67
77
 
68
- def slots(key, available:, config: :shared)
69
- 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)
70
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
71
88
  end
72
89
 
73
90
  require "kredis/types/proxy"
@@ -75,8 +92,10 @@ require "kredis/types/proxying"
75
92
 
76
93
  require "kredis/types/scalar"
77
94
  require "kredis/types/counter"
95
+ require "kredis/types/cycle"
78
96
  require "kredis/types/flag"
79
97
  require "kredis/types/enum"
98
+ require "kredis/types/hash"
80
99
  require "kredis/types/list"
81
100
  require "kredis/types/unique_list"
82
101
  require "kredis/types/set"
@@ -1,3 +1,3 @@
1
1
  module Kredis
2
- VERSION = "0.2.2"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/kredis.rb CHANGED
@@ -1,20 +1,30 @@
1
- require "kredis/railtie"
1
+ require "active_support"
2
+ require "active_support/core_ext/module/attribute_accessors"
3
+
2
4
  require "kredis/version"
3
5
 
4
6
  require "kredis/connections"
7
+ require "kredis/log_subscriber"
5
8
  require "kredis/namespace"
9
+ require "kredis/type_casting"
6
10
  require "kredis/types"
7
11
  require "kredis/attributes"
8
- require "kredis/type_casting"
9
12
 
10
- module Kredis
11
- include Connections, Namespace, Types, TypeCasting
13
+ require "kredis/railtie" if defined?(Rails::Railtie)
12
14
 
15
+ module Kredis
16
+ include Connections, Namespace, TypeCasting, Types
13
17
  extend self
14
18
 
19
+ autoload :Migration, "kredis/migration"
20
+
15
21
  mattr_accessor :logger
16
22
 
17
23
  def redis(config: :shared)
18
24
  configured_for(config)
19
25
  end
26
+
27
+ def instrument(channel, **options, &block)
28
+ ActiveSupport::Notifications.instrument("#{channel}.kredis", **options, &block)
29
+ end
20
30
  end
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kredis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.4.0
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-02-05 00:00:00.000000000 Z
12
+ date: 2021-09-15 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,7 +39,21 @@ dependencies:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '4.2'
42
- description:
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
56
+ description:
43
57
  email: david@hey.com
44
58
  executables: []
45
59
  extensions: []
@@ -50,14 +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
77
+ - lib/kredis/types/cycle.rb
59
78
  - lib/kredis/types/enum.rb
60
79
  - lib/kredis/types/flag.rb
80
+ - lib/kredis/types/hash.rb
61
81
  - lib/kredis/types/list.rb
62
82
  - lib/kredis/types/proxy.rb
63
83
  - lib/kredis/types/proxy/failsafe.rb
@@ -71,7 +91,7 @@ homepage: https://github.com/rails/kredis
71
91
  licenses:
72
92
  - MIT
73
93
  metadata: {}
74
- post_install_message:
94
+ post_install_message:
75
95
  rdoc_options: []
76
96
  require_paths:
77
97
  - lib
@@ -86,8 +106,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
106
  - !ruby/object:Gem::Version
87
107
  version: '0'
88
108
  requirements: []
89
- rubygems_version: 3.1.2
90
- signing_key:
109
+ rubygems_version: 3.1.4
110
+ signing_key:
91
111
  specification_version: 4
92
112
  summary: Higher-level data structures built on Redis.
93
113
  test_files: []