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 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