kredis 0.1.0 → 0.2.2

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: a6a806a43280483f33a61cb2133b869d7742251eb7df3ccfcfe18c4bc5274873
4
- data.tar.gz: 1c255fa02c7172090724ee01b5f72ec5540b4e38c43505135bd123c2ab6a1c79
3
+ metadata.gz: f3e3619ec3a977b472348572202b324c813d550d43cc866ec1aab7dad8e463f0
4
+ data.tar.gz: 920c49f0cfe443a71dd5dc8f112070ab8061bbe2d352c98b45026ceeed871f2e
5
5
  SHA512:
6
- metadata.gz: 270f4fda9ed70fb489e9b54431ae01118f2f25fcb6632a8eb83abb50e4ff0871729a4282989c75801bd6f0c7520f72d51923b6980c46089f0f27a3d8145a2a6e
7
- data.tar.gz: d1d5cc05fbe9091db57a20ad138e08cf1ee60d00765ebf69222cf53f9c153ce0a260496d0dd406c807ef1b6b51164ab365487c72e9299ca703d776add411c564
6
+ metadata.gz: 8f97bda6b251a100411c0338d2760aa1c2565f3ee1901f5f4971123cbe8b7a49fabf64ccc210775e9694263fd1a2a142641bcf81659776f746d05163640afda5
7
+ data.tar.gz: 546b9246e3952a92452fe55cf574233b12d50f29bdeb345c7efe3ebf86bd3ab9a6ff4d22731438ae3258586caca530c1e7229ffee9b581a4a22b04f585dd4467
data/README.md CHANGED
@@ -1,11 +1,161 @@
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
+ 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
72
+ enum.value = "four"
73
+ "two" == enum.value
74
+ enum.reset
75
+ "one" == enum.value
76
+
77
+ 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
90
+
91
+ 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?
100
+ sleep 0.5.seconds
101
+ true == flag.marked?
102
+ sleep 0.6.seconds
103
+ false == flag.marked?
104
+ ```
105
+
106
+ And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`:
107
+
108
+ ```ruby
109
+ one_string = Kredis.string "mystring"
110
+ two_string = Kredis.string "mystring", config: :secondary
111
+
112
+ one_string.value = "just on shared"
113
+ two_string.value != one_string.value
114
+ ```
115
+
116
+ You can use all these structures in models:
117
+
118
+ ```ruby
119
+ class Person < ApplicationRecord
120
+ kredis_list :names
121
+ kredis_list :names_with_custom_key, key: ->(p) { "person:#{p.id}:names_customized" }
122
+ kredis_unique_list :skills, limit: 2
123
+ kredis_enum :morning, values: %w[ bright blue black ], default: "bright"
124
+ end
125
+
126
+ 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?
131
+ ```
132
+
133
+
134
+ ## Installation
135
+
136
+ 1. Add the `kredis` gem to your Gemfile: `gem 'kredis'`
137
+ 2. Run `./bin/bundle install`
138
+ 3. Add a default configuration under `config/redis/shared.yml`
139
+
140
+ A default configuration can look like this for `config/redis/shared.yml`:
141
+
142
+ ```yaml
143
+ production: &production
144
+ host: <%= ENV.fetch("REDIS_SHARED_HOST", "127.0.0.1") %>
145
+ port: <%= ENV.fetch("REDIS_SHARED_PORT", "6379") %>
146
+ timeout: 1
147
+
148
+ development: &development
149
+ host: <%= ENV.fetch("REDIS_SHARED_HOST", "127.0.0.1") %>
150
+ port: <%= ENV.fetch("REDIS_SHARED_PORT", "6379") %>
151
+ timeout: 1
152
+
153
+ test:
154
+ <<: *development
155
+ ```
156
+
157
+ 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
158
 
8
- ...
9
159
 
10
160
  ## License
11
161
 
data/lib/kredis.rb CHANGED
@@ -1,14 +1,20 @@
1
1
  require "kredis/railtie"
2
+ require "kredis/version"
2
3
 
3
4
  require "kredis/connections"
5
+ require "kredis/namespace"
4
6
  require "kredis/types"
5
7
  require "kredis/attributes"
6
- require "kredis/namespace"
8
+ require "kredis/type_casting"
7
9
 
8
10
  module Kredis
9
- include Connections
10
- include Namespace
11
- include Types
11
+ include Connections, Namespace, Types, TypeCasting
12
12
 
13
13
  extend self
14
+
15
+ mattr_accessor :logger
16
+
17
+ def redis(config: :shared)
18
+ configured_for(config)
19
+ end
14
20
  end
@@ -2,33 +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
5
+ def kredis_proxy(name, key: nil, config: :shared)
6
+ kredis_connection_with __method__, name, key, config: config
7
+ end
8
+
9
+ def kredis_string(name, key: nil, config: :shared)
10
+ kredis_connection_with __method__, name, key, config: config
11
+ end
12
+
13
+ def kredis_integer(name, key: nil, config: :shared)
14
+ kredis_connection_with __method__, name, key, config: config
15
+ end
16
+
17
+ def kredis_datetime(name, key: nil, config: :shared)
18
+ kredis_connection_with __method__, name, key, config: config
19
+ end
20
+
21
+ def kredis_flag(name, key: nil, config: :shared)
22
+ kredis_connection_with __method__, name, key, config: config
23
+
24
+ define_method("#{name}?") do
25
+ send(name).marked?
14
26
  end
15
27
  end
16
28
 
17
- def kredis_unique_list(name, limit: nil, config: :shared)
18
- ivar_symbol = :"@#{name}_kredis_unique_list"
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
19
56
 
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))
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
25
68
  end
26
69
  end
27
- end
28
70
  end
29
71
 
30
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
+
31
80
  def kredis_key_for_attribute(name)
32
- "#{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."
33
86
  end
34
87
  end
@@ -5,10 +5,14 @@ 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
15
+ logger&.info "[Kredis] Connections all cleared"
12
16
  connections.each_value(&:flushdb)
13
17
  end
14
18
  end
@@ -0,0 +1,51 @@
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
+ @copy_sha = @redis.script "load", "redis.call('SETNX', KEYS[2], redis.call('GET', KEYS[1])); return 1;"
9
+ end
10
+
11
+ def migrate_all(key_pattern)
12
+ each_key_batch_matching(key_pattern) do |keys|
13
+ keys.each do |key|
14
+ ids = key.scan(/\d+/).map(&:to_i)
15
+ migrate from: key, to: yield(key, *ids)
16
+ end
17
+ end
18
+ end
19
+
20
+ def migrate(from:, to:)
21
+ namespaced_to = Kredis.namespaced_key(to)
22
+
23
+ if to.present? && from != namespaced_to
24
+ log_migration "Migrating key #{from} to #{namespaced_to}"
25
+ @redis.evalsha @copy_sha, keys: [ from, namespaced_to ]
26
+ else
27
+ log_migration "Skipping blank/unaltered migration key #{from} → #{to}"
28
+ end
29
+ end
30
+
31
+ def delete_all(key_pattern)
32
+ each_key_batch_matching(key_pattern) do |keys|
33
+ @redis.del *keys
34
+ end
35
+ end
36
+
37
+ private
38
+ SCAN_BATCH_SIZE = 1_000
39
+
40
+ def each_key_batch_matching(key_pattern, &block)
41
+ cursor = "0"
42
+ begin
43
+ cursor, keys = @redis.scan(cursor, match: key_pattern, count: SCAN_BATCH_SIZE)
44
+ @redis.pipelined { yield keys }
45
+ end until cursor == "0"
46
+ end
47
+
48
+ def log_migration(message)
49
+ Kredis.logger&.debug "[Kredis Migration] #{message}"
50
+ end
51
+ end
@@ -3,26 +3,30 @@ require "rails/railtie"
3
3
  module Kredis
4
4
  class Railtie < ::Rails::Railtie
5
5
  config.kredis = ActiveSupport::OrderedOptions.new
6
- config.eager_load_namespaces << Kredis
7
6
 
8
7
  initializer "kredis.testing" do
9
8
  ActiveSupport.on_load(:active_support_test_case) do
10
- parallelize_setup { |worker| Kredis.namespace = "test-#{worker}" }
11
- parallelize_teardown { Kredis.clear_all }
9
+ parallelize_setup { |worker| Kredis.namespace = "test-#{worker}" }
10
+ teardown { Kredis.clear_all }
12
11
  end
13
12
  end
14
13
 
14
+ initializer "kredis.logger" do
15
+ Kredis.logger = config.kredis.logger || Rails.logger
16
+ end
17
+
15
18
  initializer "kredis.configurator" do
16
19
  Kredis.configurator = Rails.application
17
20
  end
18
21
 
19
22
  initializer "kredis.attributes" do
20
- ActiveSupport.on_load(:active_model) do
21
- ActiveModel::Base.send :include, Kredis::Attributes
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)
22
26
  end
23
27
 
24
28
  ActiveSupport.on_load(:active_record) do
25
- ActiveRecord::Base.send :include, Kredis::Attributes
29
+ include Kredis::Attributes
26
30
  end
27
31
  end
28
32
  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,27 +1,83 @@
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
8
21
  end
9
22
 
10
- def unique_list(key, limit: nil, config: :shared)
11
- UniqueList.new configured_for(config), namespaced_key(key), limit: limit
23
+ def float(key, default: nil, config: :shared)
24
+ Scalar.new configured_for(config), namespaced_key(key), typed: :float, default: default
12
25
  end
13
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
33
+ end
34
+
35
+ def json(key, default: nil, config: :shared)
36
+ Scalar.new configured_for(config), namespaced_key(key), typed: :json, default: default
37
+ end
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
 
18
- def mutex(key, expires_in: nil, config: :shared)
19
- Mutex.new configured_for(config), namespaced_key(key), expires_in: expires_in
44
+ def flag(key, config: :shared)
45
+ Flag.new configured_for(config), namespaced_key(key)
46
+ end
47
+
48
+ def enum(key, values:, default:, config: :shared)
49
+ Enum.new configured_for(config), namespaced_key(key), values: values, default: default
50
+ end
51
+
52
+ def list(key, typed: :string, config: :shared)
53
+ List.new configured_for(config), namespaced_key(key), typed: typed
54
+ end
55
+
56
+ def unique_list(key, typed: :string, limit: nil, config: :shared)
57
+ UniqueList.new configured_for(config), namespaced_key(key), typed: typed, limit: limit
58
+ end
59
+
60
+ def set(key, typed: :string, config: :shared)
61
+ Set.new configured_for(config), namespaced_key(key), typed: typed
62
+ end
63
+
64
+ def slot(key, config: :shared)
65
+ Slots.new configured_for(config), namespaced_key(key), available: 1
66
+ end
67
+
68
+ def slots(key, available:, config: :shared)
69
+ Slots.new configured_for(config), namespaced_key(key), available: available
20
70
  end
21
71
  end
22
72
 
23
- require "kredis/proxy"
73
+ require "kredis/types/proxy"
74
+ require "kredis/types/proxying"
75
+
76
+ require "kredis/types/scalar"
77
+ require "kredis/types/counter"
78
+ require "kredis/types/flag"
79
+ require "kredis/types/enum"
24
80
  require "kredis/types/list"
25
81
  require "kredis/types/unique_list"
26
- require "kredis/types/counter"
27
- require "kredis/types/mutex"
82
+ require "kredis/types/set"
83
+ require "kredis/types/slots"
@@ -1,16 +1,22 @@
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
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
@@ -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
@@ -0,0 +1,15 @@
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
6
+ end
7
+
8
+ def marked?
9
+ exists?
10
+ end
11
+
12
+ def remove
13
+ del
14
+ end
15
+ end
@@ -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
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
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
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
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.0"
2
+ VERSION = "0.2.2"
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.0
4
+ version: 0.2.2
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-05 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,20 +50,28 @@ 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/enum.rb
60
+ - lib/kredis/types/flag.rb
58
61
  - lib/kredis/types/list.rb
59
- - lib/kredis/types/mutex.rb
62
+ - lib/kredis/types/proxy.rb
63
+ - lib/kredis/types/proxy/failsafe.rb
64
+ - lib/kredis/types/proxying.rb
65
+ - lib/kredis/types/scalar.rb
66
+ - lib/kredis/types/set.rb
67
+ - lib/kredis/types/slots.rb
60
68
  - lib/kredis/types/unique_list.rb
61
69
  - lib/kredis/version.rb
62
70
  homepage: https://github.com/rails/kredis
63
71
  licenses:
64
72
  - MIT
65
73
  metadata: {}
66
- post_install_message:
74
+ post_install_message:
67
75
  rdoc_options: []
68
76
  require_paths:
69
77
  - lib
@@ -71,7 +79,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
79
  requirements:
72
80
  - - ">="
73
81
  - !ruby/object:Gem::Version
74
- version: 2.6.0
82
+ version: 2.7.0
75
83
  required_rubygems_version: !ruby/object:Gem::Requirement
76
84
  requirements:
77
85
  - - ">="
@@ -79,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
87
  version: '0'
80
88
  requirements: []
81
89
  rubygems_version: 3.1.2
82
- signing_key:
90
+ signing_key:
83
91
  specification_version: 4
84
92
  summary: Higher-level data structures built on Redis.
85
93
  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