rails_rate_limit 0.1.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf52b273c158732c58a654255744cbc096ddab47348f16684211435074dfd9b9
4
- data.tar.gz: 239cf6c9e06d1300d057c28a2bdcd065de265799d58c369e061e9787e504879b
3
+ metadata.gz: 898367675a14db6a2edf98d6e052797f4447896e57bc180702c897372f8c2ab4
4
+ data.tar.gz: 4870de49e9e6895566ff929821b4712d5272efddc2154c474bf7ce76653b4d85
5
5
  SHA512:
6
- metadata.gz: 17198b2b17fe32b8dea0d56d99a02157cc4e0f50e103006b6faea197d38b2206c9810243cdf06a1c0e3c95807bd63d00feea07a426f358f026d60c7c57a5e37b
7
- data.tar.gz: b1b66ee43016fd1db38083833228c779507dd586573cd9d47a3d69bd0a5cac45e6c478308a9e399d370fe5618b9182c657c55e8b09ff0e4ca465a6dbe25a2e6e
6
+ metadata.gz: 651e61eeeb9ca99c8e80b03103d27c18d83e0c1d958d3b8e1dd2b5561537df219be3e9c889f5ac79d615416b9317588be7fb0ed5e0ebffa193386dd98b9db7f2
7
+ data.tar.gz: d19a7092d505a0ded484c14335d12652f621543b001742248e4fc61d23628442624e536346e979ea4701df9ee63e8c59ceca488df8868b1f5749c60dce4dd4ff
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Rails Rate Limit
1
+ # **Rails Rate Limit**
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rails_rate_limit.svg)](https://badge.fury.io/rb/rails_rate_limit)
4
4
  [![Build Status](https://github.com/kasvit/rails_rate_limit/workflows/Ruby/badge.svg)](https://github.com/kasvit/rails_rate_limit/actions)
@@ -9,6 +9,17 @@ For example, if you set a limit of 100 requests per hour, and a user makes 100 r
9
9
 
10
10
  The gem supports rate limiting for both HTTP requests (in controllers) and instance method calls (in any Ruby class), with multiple storage backends (Redis, Memcached, Memory).
11
11
 
12
+ ## Features
13
+
14
+ - Multiple storage backends (Redis, Memcached, Memory)
15
+ - Sliding window algorithm for accurate rate limiting
16
+ - Support for both controllers and Ruby classes
17
+ - Multiple rate limits for controllers
18
+ - Custom rate limit names and skipping for controllers
19
+ - Flexible configuration options
20
+ - Automatic HTTP headers
21
+ - Custom error handlers
22
+
12
23
  ## Installation
13
24
 
14
25
  Add this line to your application's Gemfile:
@@ -23,46 +34,53 @@ And then execute:
23
34
  $ bundle install
24
35
  ```
25
36
 
37
+ Generate the initializer:
38
+
39
+ ```bash
40
+ $ rails generate rails_rate_limit:install
41
+ ```
42
+
43
+ This will create a configuration file at `config/initializers/rails_rate_limit.rb` with all available options commented out.
44
+
26
45
  ## Configuration
27
46
 
28
- Create an initializer `config/initializers/rails_rate_limit.rb`:
47
+ The generated initializer (`config/initializers/rails_rate_limit.rb`) includes all available configuration options with default values commented out. You can uncomment and modify the options you want to customize:
29
48
 
30
49
  ```ruby
31
50
  RailsRateLimit.configure do |config|
32
- # Optional: Choose your storage backend (default: :memory)
33
- config.default_store = :redis # Available options: :redis, :memcached, :memory
34
-
35
- # Optional: Configure Redis connection (required if using Redis store)
36
- config.redis_connection = Redis.new(
37
- url: ENV['REDIS_URL'],
38
- timeout: 1,
39
- reconnect_attempts: 2
40
- )
41
-
42
- # Optional: Configure Memcached connection (required if using Memcached store)
43
- config.memcached_connection = Dalli::Client.new(
44
- ENV['MEMCACHED_URL'],
45
- { expires_in: 1.day, compress: true }
46
- )
47
-
48
- # Optional: Configure logging
49
- # set `nil` to disable logging
50
- config.logger = Rails.logger
51
-
52
- # Optional: Configure default handler for controllers (HTTP requests)
53
- config.handle_controller_exceeded = -> {
54
- # Default handler returns a JSON response with a 429 status code
55
- render json: {
56
- error: "Too many requests",
57
- retry_after: response.headers["Retry-After"]
58
- }, status: :too_many_requests
59
- }
60
-
61
- # Optional: Configure default handler for methods
51
+ # Choose your storage backend (default: :memory)
52
+ # Available options: :redis, :memcached, :memory
53
+ # config.default_store = :memory
54
+
55
+ # Configure Redis connection (required if using Redis store)
56
+ # config.redis_connection = Redis.new(
57
+ # url: ENV['REDIS_URL'],
58
+ # timeout: 1,
59
+ # reconnect_attempts: 2
60
+ # )
61
+
62
+ # Configure Memcached connection (required if using Memcached store)
63
+ # config.memcached_connection = Dalli::Client.new(
64
+ # ENV['MEMCACHED_URL'],
65
+ # { expires_in: 1.day, compress: true }
66
+ # )
67
+
68
+ # Configure logging (set to nil to disable logging)
69
+ # config.logger = Rails.logger
70
+
71
+ # Configure default handler for controllers (HTTP requests)
72
+ # config.handle_controller_exceeded = -> {
73
+ # render json: {
74
+ # error: "Too many requests",
75
+ # retry_after: response.headers["Retry-After"]
76
+ # }, status: :too_many_requests
77
+ # }
78
+
79
+ # Configure default handler for methods
62
80
  # By default, it raises RailsRateLimit::RateLimitExceeded
63
- config.handle_klass_exceeded = -> {
64
- raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
65
- }
81
+ # config.handle_klass_exceeded = -> {
82
+ # raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
83
+ # }
66
84
  end
67
85
  ```
68
86
 
@@ -73,8 +91,8 @@ end
73
91
  Include the module and set rate limits for your controllers:
74
92
 
75
93
  ```ruby
76
- class ApiController < ApplicationController
77
- include RailsRateLimit::Controller # include this module
94
+ class UsersController < ApplicationController
95
+ include RailsRateLimit::Controller # include this module
78
96
 
79
97
  # Basic usage - limit all actions
80
98
  set_rate_limit limit: 100, # Maximum requests allowed
@@ -93,29 +111,114 @@ class ApiController < ApplicationController
93
111
  plan_limit: current_user.plan.limit,
94
112
  upgrade_url: pricing_url
95
113
  }, status: :too_many_requests
96
- }
114
+ },
115
+ as: :custom_rate_limit # Custom name for rate limit (optional)
116
+ end
117
+ ```
118
+
119
+ ### Multiple Rate Limits
120
+
121
+ You can set multiple rate limits for a single controller or his ancestors. Each rate limit can have its own configuration:
122
+
123
+ ```ruby
124
+ class ApiController < ApplicationController
125
+ include RailsRateLimit::Controller
126
+
127
+ # Global rate limit for all actions
128
+ set_rate_limit limit: 1000,
129
+ period: 1.hour,
130
+ as: :global_rate_limit # Custom name for rate limit (optional)
131
+
132
+ # Stricter limit for write operations
133
+ set_rate_limit only: [:create, :update, :destroy],
134
+ limit: 100,
135
+ period: 1.hour,
136
+ as: :write_operations_limit # Custom name for rate limit (optional)
137
+
138
+ # Custom limit for specific action
139
+ set_rate_limit only: [:expensive_operation],
140
+ limit: 10,
141
+ period: 1.day,
142
+ as: :expensive_operation_limit # Custom name for rate limit (optional)
143
+ end
144
+ ```
145
+
146
+ ### Custom Rate Limit Names
147
+
148
+ You can give your rate limits custom names using the `as` option. This is useful for:
149
+ - Better logging and debugging
150
+ - Skipping specific rate limits
151
+ - Better organization of multiple limits
152
+
153
+ ```ruby
154
+ class ApplicationController < ActionController::API
155
+ include RailsRateLimit::Controller
156
+
157
+ # Global rate limit that applies to all inherited controllers
158
+ set_rate_limit limit: 1000,
159
+ period: 1.hour,
160
+ as: :global_rate_limit
161
+ end
162
+
163
+ class ApiController < ApplicationController
164
+ # Additional limit for API endpoints
165
+ set_rate_limit limit: 100,
166
+ period: 1.minute,
167
+ as: :api_rate_limit
168
+ end
169
+ ```
170
+
171
+ ### Skipping Rate Limits
172
+
173
+ You can skip specific rate limits for certain actions using `skip_before_action`:
174
+
175
+ ```ruby
176
+ class PaymentsController < ApiController
177
+ # Skip global rate limit for webhook endpoint
178
+ skip_before_action :global_rate_limit, only: [:webhook]
179
+
180
+ # Skip API rate limit for status check
181
+ skip_before_action :api_rate_limit, only: [:status]
182
+
183
+ def webhook
184
+ # This action will ignore global rate limit (custom name)
185
+ end
186
+
187
+ def status
188
+ # This action will ignore API rate limit (custom name)
189
+ end
97
190
  end
98
191
  ```
99
192
 
100
193
  ### Rate Limiting Methods
101
194
 
102
- You can limit any instance method in your classes (class methods are not supported yet):
195
+ You can limit both instance and class methods in your classes:
103
196
 
104
197
  ```ruby
105
198
  class ApiClient
106
- include RailsRateLimit::Klass # include this module
199
+ include RailsRateLimit::Klass
107
200
 
201
+ # Instance method
108
202
  def make_request
109
203
  # Your API call logic here
110
204
  end
111
205
 
112
- # IMPORTANT: set_rate_limit must be called AFTER method definition
113
- # Basic usage
206
+ # Class method
207
+ def self.bulk_request
208
+ # Your API call logic here
209
+ end
210
+
211
+ # Rate limit for instance method
114
212
  set_rate_limit :make_request,
115
213
  limit: 100,
116
214
  period: 1.minute
117
215
 
118
- # Advanced usage with all options
216
+ # Rate limit for class method
217
+ set_rate_limit :bulk_request,
218
+ limit: 10,
219
+ period: 1.hour
220
+
221
+ # Advanced usage with all options (instance method)
119
222
  set_rate_limit :another_method,
120
223
  limit: 10, # Maximum calls allowed
121
224
  period: 1.hour, # Time window for the limit
@@ -128,21 +231,26 @@ class ApiClient
128
231
  nil # Method will return nil
129
232
  }
130
233
 
131
- # Example with default handler that raises error
132
- set_rate_limit :risky_method,
133
- limit: 5,
134
- period: 1.minute
135
- # Without on_exceeded option it will use default handler
136
- # that raises RailsRateLimit::RateLimitExceeded
137
-
138
- # Example with custom error handling
139
- def safe_request
140
- risky_method
141
- rescue RailsRateLimit::RateLimitExceeded => e
142
- # Handle the error
143
- Rails.logger.warn("Rate limit exceeded: #{e.message}")
144
- nil # or any other fallback value
145
- end
234
+ # Advanced usage with all options (class method)
235
+ set_rate_limit :another_class_method,
236
+ limit: 5, # Maximum calls allowed
237
+ period: 1.day, # Time window for the limit
238
+ by: -> { "global:#{name}" }, # Method call identifier
239
+ store: :redis, # Override default store
240
+ on_exceeded: -> { # Custom error handler
241
+ log_exceeded_event
242
+ "Rate limit exceeded" # Return custom message
243
+ }
244
+
245
+ # Direct rate limit setting
246
+ # You can also set rate limits directly if you have both instance and class methods with the same name
247
+ set_instance_rate_limit :process, # For instance method
248
+ limit: 10,
249
+ period: 1.hour
250
+
251
+ set_class_rate_limit :process, # For class method
252
+ limit: 5,
253
+ period: 1.hour
146
254
  end
147
255
  ```
148
256
 
@@ -153,11 +261,13 @@ For both controllers and methods:
153
261
  - `period`: (Required) Time period for the limit (in seconds or ActiveSupport::Duration)
154
262
  - `by`: (Optional) Lambda/Proc to generate unique identifier
155
263
  - Default for controllers: `"#{controller.class.name}:#{controller.request.remote_ip}"`
156
- - Default for methods: `"#{self.class.name}##{method_name}:#{respond_to?(:id) ? 'id='+id.to_s : 'object_id='+object_id.to_s}"`
264
+ - Default for instance methods: `"#{self.class.name}##{method_name}:#{respond_to?(:id) ? 'id='+id.to_s : 'object_id='+object_id.to_s}"`
265
+ - Default for class methods: `"#{class.name}.#{method_name}"`
157
266
  - `store`: (Optional) Override default storage backend (`:redis`, `:memcached`, `:memory`)
158
267
  - `on_exceeded`: (Optional) Custom handler for rate limit exceeded
159
268
 
160
269
  Additional options for controllers:
270
+ - `as`: (Optional) Custom name for rate limit
161
271
  - `only`: (Optional) Array of action names to limit
162
272
  - `except`: (Optional) Array of action names to exclude
163
273
 
@@ -188,6 +298,7 @@ By default, the gem logs the error message to the logger together with your cust
188
298
  # where key for klass is `by` or default unique identifier
189
299
  # Rate limit exceeded for ReportGenerator#generate:object_id=218520. Limit: 2 requests per 10 seconds
190
300
  # Rate limit exceeded for Notification#deliver:id=1. Limit: 3 requests per 60 seconds
301
+ # Rate limit exceeded for ReportGenerator.generate. Limit: 2 requests per 10 seconds
191
302
 
192
303
  # where key for controller is `by` or default unique identifier
193
304
  # Rate limit exceeded for HomeController:127.0.0.1. Limit: 100 requests per 1 minute
@@ -204,6 +315,13 @@ For controller rate limiting, the following headers are automatically added:
204
315
 
205
316
  ## Storage Backends
206
317
 
318
+ ### Memory (Default)
319
+ - No additional dependencies
320
+ - Perfect for development or single-server setups
321
+ - Data is lost on server restart
322
+ - Not suitable for distributed systems
323
+ - Thread-safe implementation
324
+
207
325
  ### Redis
208
326
  - Requires the `redis-rails` gem
209
327
  - Best for distributed systems
@@ -218,13 +336,6 @@ For controller rate limiting, the following headers are automatically added:
218
336
  - Works well in distributed environments
219
337
  - Good option if you're already using Memcached
220
338
 
221
- ### Memory (Default)
222
- - No additional dependencies
223
- - Perfect for development or single-server setups
224
- - Data is lost on server restart
225
- - Not suitable for distributed systems
226
- - Thread-safe implementation
227
-
228
339
  ## Development
229
340
 
230
341
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module RailsRateLimit
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+ desc "Creates a RailsRateLimit initializer file at config/initializers"
10
+
11
+ def copy_initializer
12
+ template "initializer.rb", "config/initializers/rails_rate_limit.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ RailsRateLimit.configure do |config|
4
+ # Choose your storage backend (default: :memory)
5
+ # Available options: :redis, :memcached, :memory
6
+ # config.default_store = :memory
7
+
8
+ # Configure Redis connection (required if using Redis store)
9
+ # config.redis_connection = Redis.new(
10
+ # url: ENV['REDIS_URL'],
11
+ # timeout: 1,
12
+ # reconnect_attempts: 2
13
+ # )
14
+
15
+ # Configure Memcached connection (required if using Memcached store)
16
+ # config.memcached_connection = Dalli::Client.new(
17
+ # ENV['MEMCACHED_URL'],
18
+ # { expires_in: 1.day, compress: true }
19
+ # )
20
+
21
+ # Configure logging (set to nil to disable logging)
22
+ # config.logger = Rails.logger
23
+
24
+ # Configure default handler for controllers (HTTP requests)
25
+ # config.handle_controller_exceeded = -> {
26
+ # render json: {
27
+ # error: "Too many requests",
28
+ # retry_after: response.headers["Retry-After"]
29
+ # }, status: :too_many_requests
30
+ # }
31
+
32
+ # Configure default handler for methods
33
+ # By default, it raises RailsRateLimit::RateLimitExceeded
34
+ # config.handle_klass_exceeded = -> {
35
+ # raise RailsRateLimit::RateLimitExceeded, "Rate limit exceeded"
36
+ # }
37
+ end
@@ -1,31 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
+ require "active_support/core_ext/string/inflections"
5
+ require "active_support/core_ext/class/attribute"
4
6
 
5
7
  module RailsRateLimit
6
8
  module Controller
7
9
  extend ActiveSupport::Concern
8
10
 
11
+ included do
12
+ class_attribute :rate_limits, default: []
13
+ class_attribute :skipped_rate_limits, default: []
14
+ end
15
+
9
16
  class_methods do
10
17
  def set_rate_limit(limit:, period:, by: nil, on_exceeded: nil, store: nil, **options)
11
18
  Validations.validate_options!(limit: limit, period: period, by: by, on_exceeded: on_exceeded, store: store)
12
19
 
13
- before_action(options) do |controller|
14
- limiter = RateLimiter.new(
15
- context: controller,
16
- by: by || "#{controller.class.name}:#{controller.request.remote_ip}",
17
- limit: limit,
18
- period: period.to_i,
19
- store: store
20
- )
21
-
22
- begin
23
- limiter.perform!
24
- rescue RailsRateLimit::RateLimitExceeded
25
- handler = on_exceeded.nil? ? RailsRateLimit.configuration.handle_controller_exceeded : on_exceeded
26
- controller.instance_exec(&handler)
20
+ callback_name = options.delete(:as) || :"check_rate_limit_#{name.to_s.underscore}_#{rate_limits.length}"
21
+
22
+ self.rate_limits = rate_limits + [{
23
+ limit: limit,
24
+ period: period,
25
+ by: by,
26
+ on_exceeded: on_exceeded,
27
+ store: store,
28
+ callback_name: callback_name
29
+ }]
30
+
31
+ define_method(callback_name) do
32
+ self.class.rate_limits.each do |rate_limit|
33
+ next if self.class.skipped_rate_limits.include?(rate_limit[:callback_name])
34
+
35
+ limiter = RateLimiter.new(
36
+ context: self,
37
+ by: rate_limit[:by] || "#{self.class.name}:#{request.remote_ip}",
38
+ limit: rate_limit[:limit],
39
+ period: rate_limit[:period].to_i,
40
+ store: rate_limit[:store]
41
+ )
42
+
43
+ begin
44
+ limiter.perform!
45
+ rescue RailsRateLimit::RateLimitExceeded
46
+ handler = rate_limit[:on_exceeded].nil? ? RailsRateLimit.configuration.handle_controller_exceeded : rate_limit[:on_exceeded]
47
+ return instance_exec(&handler)
48
+ end
27
49
  end
28
50
  end
51
+
52
+ before_action callback_name, **options
53
+ end
54
+
55
+ def skip_before_action(callback_name, **options)
56
+ super
57
+ self.skipped_rate_limits = skipped_rate_limits + [callback_name]
58
+ end
59
+
60
+ def inherited(subclass)
61
+ super
62
+ subclass.rate_limits = rate_limits.dup
63
+ subclass.skipped_rate_limits = skipped_rate_limits.dup
29
64
  end
30
65
  end
31
66
  end
@@ -8,6 +8,18 @@ module RailsRateLimit
8
8
  def set_rate_limit(method_name, limit:, period:, by: nil, store: nil, on_exceeded: nil)
9
9
  Validations.validate_options!(limit: limit, period: period, by: by, store: store)
10
10
 
11
+ if method_defined?(method_name) || private_method_defined?(method_name)
12
+ set_instance_rate_limit(method_name, limit: limit, period: period, by: by, store: store,
13
+ on_exceeded: on_exceeded)
14
+ elsif singleton_class.method_defined?(method_name) || singleton_class.private_method_defined?(method_name)
15
+ set_class_rate_limit(method_name, limit: limit, period: period, by: by, store: store,
16
+ on_exceeded: on_exceeded)
17
+ else
18
+ raise ArgumentError, "Method #{method_name} is not defined"
19
+ end
20
+ end
21
+
22
+ def set_instance_rate_limit(method_name, limit:, period:, by:, store:, on_exceeded:)
11
23
  original_method = instance_method(method_name)
12
24
 
13
25
  define_method(method_name) do |*args, &block|
@@ -30,6 +42,30 @@ module RailsRateLimit
30
42
  end
31
43
  end
32
44
  end
45
+
46
+ def set_class_rate_limit(method_name, limit:, period:, by:, store:, on_exceeded:)
47
+ original_method = singleton_class.instance_method(method_name)
48
+
49
+ singleton_class.define_method(method_name) do |*args, &block|
50
+ limiter = RateLimiter.new(
51
+ context: self,
52
+ by: by || lambda {
53
+ "#{name}.#{method_name}"
54
+ },
55
+ limit: limit,
56
+ period: period.to_i,
57
+ store: store
58
+ )
59
+
60
+ begin
61
+ limiter.perform!
62
+ original_method.bind(self).call(*args, &block)
63
+ rescue RailsRateLimit::RateLimitExceeded
64
+ handler = on_exceeded.nil? ? RailsRateLimit.configuration.handle_klass_exceeded : on_exceeded
65
+ instance_exec(&handler)
66
+ end
67
+ end
68
+ end
33
69
  end
34
70
  end
35
71
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsRateLimit
4
- VERSION = "0.1.2"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_rate_limit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasvit
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-21 00:00:00.000000000 Z
11
+ date: 2025-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dalli
@@ -133,6 +133,8 @@ files:
133
133
  - LICENSE.txt
134
134
  - README.md
135
135
  - Rakefile
136
+ - lib/generators/rails_rate_limit/install/install_generator.rb
137
+ - lib/generators/rails_rate_limit/install/templates/initializer.rb
136
138
  - lib/rails_rate_limit.rb
137
139
  - lib/rails_rate_limit/configuration.rb
138
140
  - lib/rails_rate_limit/controller.rb