allowed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,9 @@
1
+ require "active_record"
2
+ require "active_support"
3
+
4
+ require "allowed/limit"
5
+ require "allowed/throttle"
6
+
7
+ ActiveSupport.on_load(:active_record) do
8
+ include Allowed::Limit
9
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+
3
+ describe Allowed do
4
+ it "includes Limit in ActiveRecord::Base" do
5
+ expect(ActiveRecord::Base.ancestors).to include(Allowed::Limit)
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ RSpec::Matchers.define :have_throttle do |limit, options|
2
+ match do |record|
3
+ record._throttles.any? do |throttle|
4
+ throttle.limit == limit && throttle.options == options
5
+ end
6
+ end
7
+ 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