allowed 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/allowed.rb +9 -0
- data/lib/allowed/limit.rb +43 -0
- data/lib/allowed/throttle.rb +48 -0
- data/spec/lib/allowed/limit_spec.rb +89 -0
- data/spec/lib/allowed/throttle_spec.rb +129 -0
- data/spec/lib/allowed_spec.rb +7 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/active_record.rb +28 -0
- data/spec/support/have_callback.rb +10 -0
- data/spec/support/have_throttle.rb +7 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8e4af22af5347dd855c58679dcd9aed1720709ac
|
4
|
+
data.tar.gz: 97dec94a7a9d9cfa1476f232aa19fcd50548fc18
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab767b716ee8ce8d01565f2921e26726b6bec321d0ef4a875fad88c39a48bd43a010a0cb33b2c8df4c158114829b308c90d3f2e8e23b08aa91dd6f7ebdad0710
|
7
|
+
data.tar.gz: c3b654d80a5c00138b85e8d6a46f95b91a53fb92635ec416aab56c82945a5135ad43b31f7a2de29f275d795171d798535964f6089d0bbd73d304319343c84fb1
|
data/lib/allowed.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Allowed
|
2
|
+
module Limit
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :_throttles
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def allow(limit, options = {}, &block)
|
11
|
+
if block_given?
|
12
|
+
options[:callback] = block
|
13
|
+
end
|
14
|
+
|
15
|
+
self._throttles ||= []
|
16
|
+
self._throttles << Throttle.new(limit, options)
|
17
|
+
|
18
|
+
validate :validate_throttles, on: :create
|
19
|
+
|
20
|
+
after_rollback :handle_throttles, on: :create
|
21
|
+
after_validation :handle_throttles, on: :create
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_throttles
|
26
|
+
@_throttle_failures.each do |throttle|
|
27
|
+
throttle.callback.call(self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
private :handle_throttles
|
31
|
+
|
32
|
+
def validate_throttles
|
33
|
+
throttles = self.class._throttles
|
34
|
+
throttles = throttles.reject { |throttle| throttle.valid?(self) }
|
35
|
+
throttles.each do |throttle|
|
36
|
+
errors.add(:base, throttle.message)
|
37
|
+
end
|
38
|
+
|
39
|
+
@_throttle_failures = throttles
|
40
|
+
end
|
41
|
+
private :validate_throttles
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Allowed
|
2
|
+
class Throttle
|
3
|
+
attr_reader :limit, :options
|
4
|
+
|
5
|
+
def initialize(limit, options = {})
|
6
|
+
@limit = limit
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def callback
|
11
|
+
options.fetch(:callback, -> (record) { })
|
12
|
+
end
|
13
|
+
|
14
|
+
def message
|
15
|
+
options.fetch(:message, "Limit reached.")
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid?(record)
|
19
|
+
return true if skip?(record)
|
20
|
+
|
21
|
+
scope_for(record).count < limit
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def scope_for(record)
|
27
|
+
scope = record.class.where("created_at >= ?", timeframe)
|
28
|
+
attributes = Array(options.fetch(:scope, []))
|
29
|
+
attributes.inject(scope) do |scope, attribute|
|
30
|
+
scope.where(attribute => record.__send__(attribute))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def skip?(record)
|
35
|
+
return unless method = options[:unless]
|
36
|
+
|
37
|
+
if method.is_a?(Symbol)
|
38
|
+
method = record.method(method).call
|
39
|
+
else
|
40
|
+
method.call(record)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def timeframe
|
45
|
+
options.fetch(:per, 1.day).ago
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Allowed::Limit do
|
4
|
+
subject { ExampleRecord }
|
5
|
+
|
6
|
+
it "defines a class variable for throttles" do
|
7
|
+
expect(subject).to respond_to(:_throttles)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe Allowed::Limit, "#allow" do
|
12
|
+
subject { ExampleRecord }
|
13
|
+
|
14
|
+
let(:limit) { 100 }
|
15
|
+
let(:block) { -> { } }
|
16
|
+
let(:options) { { message: "Over limit." } }
|
17
|
+
|
18
|
+
it "adds throttle to the record" do
|
19
|
+
subject.allow(limit, options)
|
20
|
+
|
21
|
+
expect(subject).to have_throttle(limit, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "assigns block to callback" do
|
25
|
+
subject.allow(limit, options, &block)
|
26
|
+
|
27
|
+
expect(subject).to have_throttle(limit, options.merge(callback: block))
|
28
|
+
end
|
29
|
+
|
30
|
+
it "adds validation callback" do
|
31
|
+
subject.allow(limit, options)
|
32
|
+
|
33
|
+
expect(subject).to have_callback(:validate, :validate_throttles, on: :create)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "adds after rollback callback" do
|
37
|
+
subject.allow(limit, options)
|
38
|
+
|
39
|
+
expect(subject).to have_callback(:rollback, :handle_throttles, kind: :after, on: :create)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "adds after validation callback" do
|
43
|
+
subject.allow(limit, options)
|
44
|
+
|
45
|
+
expect(subject).to have_callback(:validation, :handle_throttles, kind: :after, on: :create)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe Allowed::Limit, "#handle_throttles" do
|
50
|
+
subject { ExampleRecord.new }
|
51
|
+
|
52
|
+
let(:callback) { mock(call: true) }
|
53
|
+
let(:invalid_throttle) { mock(callback: callback) }
|
54
|
+
|
55
|
+
before do
|
56
|
+
subject.instance_variable_set("@_throttle_failures", [invalid_throttle])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "calls callback for throttle failures" do
|
60
|
+
subject.__send__(:handle_throttles)
|
61
|
+
|
62
|
+
expect(callback).to have_received(:call).with(subject).once
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe Allowed::Limit, "#validate_throttles" do
|
67
|
+
subject { ExampleRecord.new }
|
68
|
+
|
69
|
+
let(:message) { "Over limit." }
|
70
|
+
let(:valid_throttle) { mock(valid?: true) }
|
71
|
+
let(:invalid_throttle) { mock(valid?: false, message: message) }
|
72
|
+
|
73
|
+
before do
|
74
|
+
subject.class._throttles = [valid_throttle, invalid_throttle]
|
75
|
+
end
|
76
|
+
|
77
|
+
it "adds error messages to base" do
|
78
|
+
subject.__send__(:validate_throttles)
|
79
|
+
|
80
|
+
expect(subject.errors[:base].size).to eq(1)
|
81
|
+
expect(subject.errors[:base]).to include(message)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "stores throttle failures" do
|
85
|
+
subject.__send__(:validate_throttles)
|
86
|
+
|
87
|
+
expect(subject.instance_variable_get("@_throttle_failures")).to eq([invalid_throttle])
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Allowed::Throttle, ".new" do
|
4
|
+
subject { Allowed::Throttle.new(limit, options) }
|
5
|
+
|
6
|
+
let(:limit) { 100 }
|
7
|
+
let(:options) { { message: "Over limit." } }
|
8
|
+
|
9
|
+
it "sets limit" do
|
10
|
+
expect(subject.limit).to eq(limit)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "sets options" do
|
14
|
+
expect(subject.options).to eq(options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Allowed::Throttle, "#message" do
|
19
|
+
it "returns message if provided" do
|
20
|
+
message = "The message."
|
21
|
+
throttle = Allowed::Throttle.new(1, message: message)
|
22
|
+
|
23
|
+
expect(throttle.message).to eq(message)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns default message if not provided" do
|
27
|
+
throttle = Allowed::Throttle.new(1)
|
28
|
+
|
29
|
+
expect(throttle.message).to eq("Limit reached.")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe Allowed::Throttle, "#valid?, with an unless block" do
|
34
|
+
let(:record) { ExampleRecord.new }
|
35
|
+
|
36
|
+
before do
|
37
|
+
2.times { ExampleRecord.create }
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns true when skipped" do
|
41
|
+
throttle = Allowed::Throttle.new(1, unless: -> (record) { true })
|
42
|
+
|
43
|
+
expect(throttle).to be_valid(record)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns false when not skipped" do
|
47
|
+
throttle = Allowed::Throttle.new(1)
|
48
|
+
|
49
|
+
expect(throttle).to_not be_valid(record)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe Allowed::Throttle, "#valid?, with an unless method symbol" do
|
54
|
+
let(:record) { ExampleRecord.new }
|
55
|
+
|
56
|
+
before do
|
57
|
+
2.times { ExampleRecord.create }
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns true when skipped" do
|
61
|
+
ExampleRecord.class_eval do
|
62
|
+
def custom_method
|
63
|
+
true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
throttle = Allowed::Throttle.new(1, unless: :custom_method)
|
68
|
+
|
69
|
+
expect(throttle).to be_valid(record)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "returns false when not skipped" do
|
73
|
+
ExampleRecord.class_eval do
|
74
|
+
def custom_method
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
throttle = Allowed::Throttle.new(1, unless: :custom_method)
|
80
|
+
|
81
|
+
expect(throttle).to_not be_valid(record)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe Allowed::Throttle, "#valid?, within limit" do
|
86
|
+
subject { Allowed::Throttle.new(1) }
|
87
|
+
|
88
|
+
it "returns true" do
|
89
|
+
expect(subject).to be_valid(ExampleRecord.new)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe Allowed::Throttle, "#valid?, above limit" do
|
94
|
+
subject { Allowed::Throttle.new(1) }
|
95
|
+
|
96
|
+
before do
|
97
|
+
2.times { ExampleRecord.create }
|
98
|
+
end
|
99
|
+
|
100
|
+
it "returns false" do
|
101
|
+
expect(subject).to_not be_valid(ExampleRecord.new)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe Allowed::Throttle, "#valid?, with custom timeframe" do
|
106
|
+
subject { Allowed::Throttle.new(1, per: 5.minutes) }
|
107
|
+
|
108
|
+
before do
|
109
|
+
2.times { ExampleRecord.create(created_at: 6.minutes.ago) }
|
110
|
+
end
|
111
|
+
|
112
|
+
it "uses timeframe for count" do
|
113
|
+
expect(subject).to be_valid(ExampleRecord.new)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe Allowed::Throttle, "#valid?, with custom scope attributes" do
|
118
|
+
subject { Allowed::Throttle.new(1, scope: :user_id) }
|
119
|
+
|
120
|
+
before do
|
121
|
+
ExampleRecord.create(user_id: 2)
|
122
|
+
ExampleRecord.create(user_id: 2)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "uses scope attributes for count" do
|
126
|
+
expect(subject).to be_valid(ExampleRecord.new(user_id: 1))
|
127
|
+
expect(subject).to_not be_valid(ExampleRecord.new(user_id: 2))
|
128
|
+
end
|
129
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
Bundler.require(:default, :development)
|
4
|
+
|
5
|
+
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each do |file|
|
6
|
+
require file
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
# Use mocha as the mocking framework.
|
11
|
+
config.mock_with :mocha
|
12
|
+
|
13
|
+
# Enforce expect syntax.
|
14
|
+
config.expect_with :rspec do |rspec|
|
15
|
+
rspec.syntax = :expect
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "spec/test.db")
|
2
|
+
|
3
|
+
class ExampleRecord < ActiveRecord::Base
|
4
|
+
end
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.around do |example|
|
8
|
+
ExampleRecord._throttles = []
|
9
|
+
|
10
|
+
ActiveRecord::Base.transaction do
|
11
|
+
ActiveRecord::Migration.verbose = false
|
12
|
+
ActiveRecord::Migration.create_table(:example_records) do |table|
|
13
|
+
table.integer :user_id
|
14
|
+
table.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
example.run
|
18
|
+
|
19
|
+
raise ActiveRecord::Rollback
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
config.after(:suite) do
|
24
|
+
ActiveRecord::Base.connection.instance_variable_get("@config").tap do |configuration|
|
25
|
+
File.delete(configuration[:database])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
RSpec::Matchers.define :have_callback do |type, name, options|
|
2
|
+
match do |record|
|
3
|
+
callbacks = record.__send__(:"_#{type}_callbacks")
|
4
|
+
callbacks.any? do |callback|
|
5
|
+
callback.raw_filter == name &&
|
6
|
+
(options[:kind].nil? || callback.kind == options[:kind]) &&
|
7
|
+
(options[:on].nil? || callback.options[:on] == options[:on])
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
metadata
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: allowed
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tristan Dunn
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.3.9
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.3.9
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bourne
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.5.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.5.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 10.3.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 10.3.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.0.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.0.0
|
97
|
+
description: Throttling of ActiveRecord model creations.
|
98
|
+
email: support@dribbble.com
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- lib/allowed.rb
|
104
|
+
- lib/allowed/limit.rb
|
105
|
+
- lib/allowed/throttle.rb
|
106
|
+
- spec/lib/allowed/limit_spec.rb
|
107
|
+
- spec/lib/allowed/throttle_spec.rb
|
108
|
+
- spec/lib/allowed_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
110
|
+
- spec/support/active_record.rb
|
111
|
+
- spec/support/have_callback.rb
|
112
|
+
- spec/support/have_throttle.rb
|
113
|
+
homepage: https://github.com/dribbble/allowed
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 2.2.2
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: Throttling of ActiveRecord model creations.
|
137
|
+
test_files:
|
138
|
+
- spec/lib/allowed/limit_spec.rb
|
139
|
+
- spec/lib/allowed/throttle_spec.rb
|
140
|
+
- spec/lib/allowed_spec.rb
|
141
|
+
- spec/spec_helper.rb
|
142
|
+
- spec/support/active_record.rb
|
143
|
+
- spec/support/have_callback.rb
|
144
|
+
- spec/support/have_throttle.rb
|