lightrate-rails 1.0.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
+ SHA256:
3
+ metadata.gz: 30dc222ab05943e69909f0fed5361b5b470613bda38b6c2908114e2564d9e2a7
4
+ data.tar.gz: 3fe845e284c5378a8241fac838f885709b2438d466182f41244021afbe3d3d1a
5
+ SHA512:
6
+ metadata.gz: d39ed9be6dc10b7ad1f693c4bdeb0f42f82fd8cc0b93f83c7e6cdfecd7a7a50c049f6e801c3aa6f5c28cd65624aed08d2768d8e4ddc7859f2e72f2e5094b3480
7
+ data.tar.gz: 98014b3bb3baeacff7a236344db56e360c069e3fb1b5b5f9b548d17d2bea2036a4c09580c5a03bc82c113e718e7403e43780e270ff1b660bd9f4484ac05c181d
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in lightrate-rails.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,328 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lightrate-rails (1.0.0)
5
+ lightrate-client (~> 1.0)
6
+ rails (>= 6.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actioncable (8.0.3)
12
+ actionpack (= 8.0.3)
13
+ activesupport (= 8.0.3)
14
+ nio4r (~> 2.0)
15
+ websocket-driver (>= 0.6.1)
16
+ zeitwerk (~> 2.6)
17
+ actionmailbox (8.0.3)
18
+ actionpack (= 8.0.3)
19
+ activejob (= 8.0.3)
20
+ activerecord (= 8.0.3)
21
+ activestorage (= 8.0.3)
22
+ activesupport (= 8.0.3)
23
+ mail (>= 2.8.0)
24
+ actionmailer (8.0.3)
25
+ actionpack (= 8.0.3)
26
+ actionview (= 8.0.3)
27
+ activejob (= 8.0.3)
28
+ activesupport (= 8.0.3)
29
+ mail (>= 2.8.0)
30
+ rails-dom-testing (~> 2.2)
31
+ actionpack (8.0.3)
32
+ actionview (= 8.0.3)
33
+ activesupport (= 8.0.3)
34
+ nokogiri (>= 1.8.5)
35
+ rack (>= 2.2.4)
36
+ rack-session (>= 1.0.1)
37
+ rack-test (>= 0.6.3)
38
+ rails-dom-testing (~> 2.2)
39
+ rails-html-sanitizer (~> 1.6)
40
+ useragent (~> 0.16)
41
+ actiontext (8.0.3)
42
+ actionpack (= 8.0.3)
43
+ activerecord (= 8.0.3)
44
+ activestorage (= 8.0.3)
45
+ activesupport (= 8.0.3)
46
+ globalid (>= 0.6.0)
47
+ nokogiri (>= 1.8.5)
48
+ actionview (8.0.3)
49
+ activesupport (= 8.0.3)
50
+ builder (~> 3.1)
51
+ erubi (~> 1.11)
52
+ rails-dom-testing (~> 2.2)
53
+ rails-html-sanitizer (~> 1.6)
54
+ activejob (8.0.3)
55
+ activesupport (= 8.0.3)
56
+ globalid (>= 0.3.6)
57
+ activemodel (8.0.3)
58
+ activesupport (= 8.0.3)
59
+ activerecord (8.0.3)
60
+ activemodel (= 8.0.3)
61
+ activesupport (= 8.0.3)
62
+ timeout (>= 0.4.0)
63
+ activestorage (8.0.3)
64
+ actionpack (= 8.0.3)
65
+ activejob (= 8.0.3)
66
+ activerecord (= 8.0.3)
67
+ activesupport (= 8.0.3)
68
+ marcel (~> 1.0)
69
+ activesupport (8.0.3)
70
+ base64
71
+ benchmark (>= 0.3)
72
+ bigdecimal
73
+ concurrent-ruby (~> 1.0, >= 1.3.1)
74
+ connection_pool (>= 2.2.5)
75
+ drb
76
+ i18n (>= 1.6, < 2)
77
+ logger (>= 1.4.2)
78
+ minitest (>= 5.1)
79
+ securerandom (>= 0.3)
80
+ tzinfo (~> 2.0, >= 2.0.5)
81
+ uri (>= 0.13.1)
82
+ addressable (2.8.7)
83
+ public_suffix (>= 2.0.2, < 7.0)
84
+ ast (2.4.3)
85
+ base64 (0.3.0)
86
+ benchmark (0.4.1)
87
+ bigdecimal (3.2.3)
88
+ builder (3.3.0)
89
+ concurrent-ruby (1.3.5)
90
+ connection_pool (2.5.4)
91
+ crack (1.0.0)
92
+ bigdecimal
93
+ rexml
94
+ crass (1.0.6)
95
+ date (3.4.1)
96
+ diff-lcs (1.6.2)
97
+ docile (1.4.1)
98
+ drb (2.2.3)
99
+ erb (5.0.2)
100
+ erubi (1.13.1)
101
+ faraday (2.14.0)
102
+ faraday-net_http (>= 2.0, < 3.5)
103
+ json
104
+ logger
105
+ faraday-net_http (3.4.1)
106
+ net-http (>= 0.5.0)
107
+ faraday-retry (2.3.2)
108
+ faraday (~> 2.0)
109
+ globalid (1.3.0)
110
+ activesupport (>= 6.1)
111
+ hashdiff (1.2.1)
112
+ i18n (1.14.7)
113
+ concurrent-ruby (~> 1.0)
114
+ io-console (0.8.1)
115
+ irb (1.15.2)
116
+ pp (>= 0.6.0)
117
+ rdoc (>= 4.0.0)
118
+ reline (>= 0.4.2)
119
+ json (2.15.0)
120
+ language_server-protocol (3.17.0.5)
121
+ lightrate-client (1.0.0)
122
+ faraday (~> 2.0)
123
+ faraday-retry (~> 2.0)
124
+ json (~> 2.0)
125
+ lint_roller (1.1.0)
126
+ logger (1.7.0)
127
+ loofah (2.24.1)
128
+ crass (~> 1.0.2)
129
+ nokogiri (>= 1.12.0)
130
+ mail (2.8.1)
131
+ mini_mime (>= 0.1.1)
132
+ net-imap
133
+ net-pop
134
+ net-smtp
135
+ marcel (1.1.0)
136
+ mini_mime (1.1.5)
137
+ minitest (5.25.5)
138
+ net-http (0.6.0)
139
+ uri
140
+ net-imap (0.5.11)
141
+ date
142
+ net-protocol
143
+ net-pop (0.1.2)
144
+ net-protocol
145
+ net-protocol (0.2.2)
146
+ timeout
147
+ net-smtp (0.5.1)
148
+ net-protocol
149
+ nio4r (2.7.4)
150
+ nokogiri (1.18.10-aarch64-linux-gnu)
151
+ racc (~> 1.4)
152
+ nokogiri (1.18.10-aarch64-linux-musl)
153
+ racc (~> 1.4)
154
+ nokogiri (1.18.10-arm-linux-gnu)
155
+ racc (~> 1.4)
156
+ nokogiri (1.18.10-arm-linux-musl)
157
+ racc (~> 1.4)
158
+ nokogiri (1.18.10-arm64-darwin)
159
+ racc (~> 1.4)
160
+ nokogiri (1.18.10-x86_64-darwin)
161
+ racc (~> 1.4)
162
+ nokogiri (1.18.10-x86_64-linux-gnu)
163
+ racc (~> 1.4)
164
+ nokogiri (1.18.10-x86_64-linux-musl)
165
+ racc (~> 1.4)
166
+ parallel (1.27.0)
167
+ parser (3.3.9.0)
168
+ ast (~> 2.4.1)
169
+ racc
170
+ pp (0.6.2)
171
+ prettyprint
172
+ prettyprint (0.2.0)
173
+ prism (1.5.1)
174
+ psych (5.2.6)
175
+ date
176
+ stringio
177
+ public_suffix (6.0.2)
178
+ racc (1.8.1)
179
+ rack (3.2.1)
180
+ rack-session (2.1.1)
181
+ base64 (>= 0.1.0)
182
+ rack (>= 3.0.0)
183
+ rack-test (2.2.0)
184
+ rack (>= 1.3)
185
+ rackup (2.2.1)
186
+ rack (>= 3)
187
+ rails (8.0.3)
188
+ actioncable (= 8.0.3)
189
+ actionmailbox (= 8.0.3)
190
+ actionmailer (= 8.0.3)
191
+ actionpack (= 8.0.3)
192
+ actiontext (= 8.0.3)
193
+ actionview (= 8.0.3)
194
+ activejob (= 8.0.3)
195
+ activemodel (= 8.0.3)
196
+ activerecord (= 8.0.3)
197
+ activestorage (= 8.0.3)
198
+ activesupport (= 8.0.3)
199
+ bundler (>= 1.15.0)
200
+ railties (= 8.0.3)
201
+ rails-dom-testing (2.3.0)
202
+ activesupport (>= 5.0.0)
203
+ minitest
204
+ nokogiri (>= 1.6)
205
+ rails-html-sanitizer (1.6.2)
206
+ loofah (~> 2.21)
207
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
208
+ railties (8.0.3)
209
+ actionpack (= 8.0.3)
210
+ activesupport (= 8.0.3)
211
+ irb (~> 1.13)
212
+ rackup (>= 1.0.0)
213
+ rake (>= 12.2)
214
+ thor (~> 1.0, >= 1.2.2)
215
+ tsort (>= 0.2)
216
+ zeitwerk (~> 2.6)
217
+ rainbow (3.1.1)
218
+ rake (13.3.0)
219
+ rdoc (6.14.2)
220
+ erb
221
+ psych (>= 4.0.0)
222
+ regexp_parser (2.11.3)
223
+ reline (0.6.2)
224
+ io-console (~> 0.5)
225
+ rexml (3.4.4)
226
+ rspec (3.13.1)
227
+ rspec-core (~> 3.13.0)
228
+ rspec-expectations (~> 3.13.0)
229
+ rspec-mocks (~> 3.13.0)
230
+ rspec-core (3.13.5)
231
+ rspec-support (~> 3.13.0)
232
+ rspec-expectations (3.13.5)
233
+ diff-lcs (>= 1.2.0, < 2.0)
234
+ rspec-support (~> 3.13.0)
235
+ rspec-mocks (3.13.5)
236
+ diff-lcs (>= 1.2.0, < 2.0)
237
+ rspec-support (~> 3.13.0)
238
+ rspec-rails (6.1.5)
239
+ actionpack (>= 6.1)
240
+ activesupport (>= 6.1)
241
+ railties (>= 6.1)
242
+ rspec-core (~> 3.13)
243
+ rspec-expectations (~> 3.13)
244
+ rspec-mocks (~> 3.13)
245
+ rspec-support (~> 3.13)
246
+ rspec-support (3.13.6)
247
+ rubocop (1.81.1)
248
+ json (~> 2.3)
249
+ language_server-protocol (~> 3.17.0.2)
250
+ lint_roller (~> 1.1.0)
251
+ parallel (~> 1.10)
252
+ parser (>= 3.3.0.2)
253
+ rainbow (>= 2.2.2, < 4.0)
254
+ regexp_parser (>= 2.9.3, < 3.0)
255
+ rubocop-ast (>= 1.47.1, < 2.0)
256
+ ruby-progressbar (~> 1.7)
257
+ unicode-display_width (>= 2.4.0, < 4.0)
258
+ rubocop-ast (1.47.1)
259
+ parser (>= 3.3.7.2)
260
+ prism (~> 1.4)
261
+ rubocop-capybara (2.22.1)
262
+ lint_roller (~> 1.1)
263
+ rubocop (~> 1.72, >= 1.72.1)
264
+ rubocop-factory_bot (2.27.1)
265
+ lint_roller (~> 1.1)
266
+ rubocop (~> 1.72, >= 1.72.1)
267
+ rubocop-rspec (2.31.0)
268
+ rubocop (~> 1.40)
269
+ rubocop-capybara (~> 2.17)
270
+ rubocop-factory_bot (~> 2.22)
271
+ rubocop-rspec_rails (~> 2.28)
272
+ rubocop-rspec_rails (2.29.1)
273
+ rubocop (~> 1.61)
274
+ ruby-progressbar (1.13.0)
275
+ securerandom (0.4.1)
276
+ simplecov (0.22.0)
277
+ docile (~> 1.1)
278
+ simplecov-html (~> 0.11)
279
+ simplecov_json_formatter (~> 0.1)
280
+ simplecov-html (0.13.2)
281
+ simplecov_json_formatter (0.1.4)
282
+ stringio (3.1.7)
283
+ thor (1.4.0)
284
+ timeout (0.4.3)
285
+ tsort (0.2.0)
286
+ tzinfo (2.0.6)
287
+ concurrent-ruby (~> 1.0)
288
+ unicode-display_width (3.2.0)
289
+ unicode-emoji (~> 4.1)
290
+ unicode-emoji (4.1.0)
291
+ uri (1.0.3)
292
+ useragent (0.16.11)
293
+ vcr (6.3.1)
294
+ base64
295
+ webmock (3.25.1)
296
+ addressable (>= 2.8.0)
297
+ crack (>= 0.3.2)
298
+ hashdiff (>= 0.4.0, < 2.0.0)
299
+ websocket-driver (0.8.0)
300
+ base64
301
+ websocket-extensions (>= 0.1.0)
302
+ websocket-extensions (0.1.5)
303
+ zeitwerk (2.7.3)
304
+
305
+ PLATFORMS
306
+ aarch64-linux-gnu
307
+ aarch64-linux-musl
308
+ arm-linux-gnu
309
+ arm-linux-musl
310
+ arm64-darwin
311
+ x86_64-darwin
312
+ x86_64-linux-gnu
313
+ x86_64-linux-musl
314
+
315
+ DEPENDENCIES
316
+ bundler (~> 2.0)
317
+ lightrate-rails!
318
+ rake (~> 13.0)
319
+ rspec (~> 3.0)
320
+ rspec-rails (~> 6.0)
321
+ rubocop (~> 1.0)
322
+ rubocop-rspec (~> 2.0)
323
+ simplecov (~> 0.21)
324
+ vcr (~> 6.0)
325
+ webmock (~> 3.0)
326
+
327
+ BUNDLED WITH
328
+ 2.5.21
data/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # Lightrate Rails
2
+
3
+ A Ruby on Rails integration gem for LightRate API throttling. This gem provides seamless integration with the LightRate API using controller before_actions to automatically throttle requests using local token buckets that automatically refill from the server when needed.
4
+
5
+ ## Features
6
+
7
+ - **Controller-Level Throttling**: Use `throttle_with_lightrate` in any controller to enable automatic rate limiting
8
+ - **Flexible User Identification**: Customize user identification per-controller with full access to controller context
9
+ - **Local Token Bucket Only**: Uses only the local token bucket approach for efficient throttling with automatic server refills
10
+ - **Custom Exception Handling**: Raises `LightRateNoTokensAvailable` when no tokens are available
11
+ - **Fine-Grained Control**: Use `:only` or `:except` options to throttle specific actions
12
+ - **Controller Helpers**: Manual token consumption methods for advanced use cases
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'lightrate-rails'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ $ gem install lightrate-rails
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ ### Basic Setup
37
+
38
+ **Required:** Configure Lightrate in your Rails application initializer:
39
+
40
+ ```ruby
41
+ # config/initializers/lightrate_rails.rb
42
+
43
+ LightrateRails.configure do |config|
44
+ # Required: Your LightRate API key
45
+ config.api_key = ENV['LIGHTRATE_API_KEY']
46
+
47
+ # Required: Your LightRate Application ID
48
+ config.application_id = ENV['LIGHTRATE_APPLICATION_ID']
49
+
50
+ # Optional: Enable/disable throttling globally (default: true)
51
+ config.enabled = Rails.env.production?
52
+
53
+ # Optional: Default bucket size (default: 5)
54
+ config.default_local_bucket_size = 10
55
+ end
56
+ ```
57
+
58
+ **Note:** You must provide both a valid API key and Application ID. The gem will raise a `ConfigurationError` if either is missing.
59
+
60
+ ### Advanced Configuration
61
+
62
+ ```ruby
63
+ LightrateRails.configure do |config|
64
+ # Required: Your LightRate API key
65
+ config.api_key = ENV['LIGHTRATE_API_KEY']
66
+
67
+ # Required: Your LightRate Application ID
68
+ config.application_id = ENV['LIGHTRATE_APPLICATION_ID']
69
+
70
+ # Enable/disable throttling globally (default: true)
71
+ config.enabled = true
72
+
73
+ # Default local bucket size (default: 5)
74
+ config.default_local_bucket_size = 10
75
+
76
+ # API client configuration
77
+ config.timeout = 30
78
+ config.retry_attempts = 3
79
+ config.logger = Rails.logger
80
+ end
81
+ ```
82
+
83
+ ## Usage
84
+
85
+ ### Global Client Architecture
86
+
87
+ The gem creates a single global LightRate client instance during Rails initialization. This approach provides several benefits:
88
+
89
+ - **Efficiency**: No client creation overhead on each request
90
+ - **Shared Token Buckets**: Token buckets are shared across all requests, providing better rate limiting
91
+ - **Memory Efficiency**: Single client instance reduces memory usage
92
+ - **Consistent State**: All parts of your application use the same client configuration
93
+
94
+ ### Controller-Level Throttling
95
+
96
+ The gem automatically includes `LightrateRails::ControllerHelper` in all your controllers. To enable throttling, simply call `throttle_with_lightrate` in any controller:
97
+
98
+ #### Basic Usage
99
+
100
+ ```ruby
101
+ # Throttle all actions in this controller
102
+ class ApiController < ApplicationController
103
+ throttle_with_lightrate
104
+ end
105
+
106
+ # Only throttle specific actions
107
+ class UsersController < ApplicationController
108
+ throttle_with_lightrate only: [:create, :update, :destroy]
109
+
110
+ def index; end # NOT throttled
111
+ def show; end # NOT throttled
112
+ def create; end # Throttled
113
+ def update; end # Throttled
114
+ def destroy; end # Throttled
115
+ end
116
+
117
+ # Throttle all actions EXCEPT specific ones
118
+ class PostsController < ApplicationController
119
+ throttle_with_lightrate except: [:index, :show]
120
+
121
+ def index; end # NOT throttled
122
+ def show; end # NOT throttled
123
+ def create; end # Throttled
124
+ def update; end # Throttled
125
+ def destroy; end # Throttled
126
+ end
127
+
128
+ # Disable throttling for this entire controller
129
+ class HealthController < ApplicationController
130
+ skip_lightrate_throttling
131
+
132
+ def status; end # NOT throttled
133
+ def ping; end # NOT throttled
134
+ end
135
+ ```
136
+
137
+ #### Custom User Identification
138
+
139
+ By default, the gem uses `current_user.id` to identify users. You can customize this per-controller:
140
+
141
+ ```ruby
142
+ # Use a different method on your user object
143
+ class ApiController < ApplicationController
144
+ throttle_with_lightrate user_identifier: -> { current_user&.uuid }
145
+ end
146
+
147
+ # Use a symbol for a controller method
148
+ class ApiController < ApplicationController
149
+ throttle_with_lightrate user_identifier: :get_api_user_id
150
+
151
+ private
152
+
153
+ def get_api_user_id
154
+ request.headers['X-API-User-ID']
155
+ end
156
+ end
157
+
158
+ # Combine multiple identifiers
159
+ class ApiController < ApplicationController
160
+ throttle_with_lightrate user_identifier: -> {
161
+ "#{current_user&.id}:#{current_organization&.id}"
162
+ }
163
+ end
164
+
165
+ # Use API token for identification
166
+ class ApiV2Controller < ApplicationController
167
+ throttle_with_lightrate user_identifier: -> {
168
+ current_api_user&.token
169
+ }
170
+ end
171
+
172
+ # Combine with :only option
173
+ class ComplexController < ApplicationController
174
+ throttle_with_lightrate(
175
+ only: [:create, :update],
176
+ user_identifier: -> { request.headers['X-User-Token'] }
177
+ )
178
+ end
179
+ ```
180
+
181
+ ### Manual Token Management in Controllers
182
+
183
+ For advanced use cases, you can manually manage token consumption. The helper methods are automatically available in all controllers:
184
+
185
+ ```ruby
186
+ class ApiController < ApplicationController
187
+ def expensive_operation
188
+ # Check if tokens are available for current path before proceeding
189
+ if lightrate_tokens_available?
190
+ # Your expensive operation here
191
+ perform_expensive_operation
192
+ else
193
+ render json: { error: 'Rate limit exceeded' }, status: 429
194
+ end
195
+ end
196
+
197
+ def path_specific_operation
198
+ # Manually consume a token for a specific path
199
+ consume_lightrate_token_for_path(path: '/api/v1/special')
200
+
201
+ # Your operation here
202
+ end
203
+
204
+ def current_path_operation
205
+ # Consume a token for the current request path (most common use case)
206
+ consume_lightrate_token_for_current_path
207
+
208
+ # Your operation here
209
+ end
210
+
211
+ def conditional_operation
212
+ # Use the helper to conditionally execute code
213
+ if lightrate_tokens_available?
214
+ # Execute expensive operation
215
+ heavy_computation
216
+ else
217
+ # Fallback to lighter operation
218
+ light_computation
219
+ end
220
+ end
221
+
222
+ def custom_user_operation
223
+ # Use a different user identifier
224
+ consume_lightrate_token_for_current_path(
225
+ user_identifier: current_user.uuid
226
+ )
227
+
228
+ # Your operation here
229
+ end
230
+
231
+ def specific_path_operation
232
+ # Consume token for a specific path and method
233
+ consume_lightrate_token_for_path(
234
+ path: "/api/v1/special-endpoint",
235
+ method: "POST",
236
+ user_identifier: current_user.id
237
+ )
238
+
239
+ # Your operation here
240
+ end
241
+ end
242
+ ```
243
+
244
+ ### Controller Configuration Methods
245
+
246
+ Configure throttling behavior at the controller level:
247
+
248
+ | Method | Description | Options |
249
+ |--------|-------------|---------|
250
+ | `throttle_with_lightrate(options = {})` | Enable throttling for this controller | `only:` - Array of action names to throttle<br>`except:` - Array of action names to exclude<br>`user_identifier:` - Proc or Symbol for custom user identification |
251
+ | `skip_lightrate_throttling` | Disable throttling for this entire controller | None |
252
+
253
+ ### Available Helper Methods
254
+
255
+ The gem provides several helper methods for manual token management. All methods share a single global LightRate client instance that is created during Rails initialization, ensuring efficient token bucket management across all requests.
256
+
257
+ | Method | Description | Parameters |
258
+ |--------|-------------|------------|
259
+ | `lightrate_tokens_available?` | Check if tokens are available for current path | `user_identifier` |
260
+ | `consume_lightrate_token_for_current_path` | Consume token for current request path | `user_identifier` |
261
+ | `consume_lightrate_token_for_path` | Consume token for specific path | `path`, `method`, `user_identifier` |
262
+
263
+ **Most Common Use Cases:**
264
+
265
+ - **`lightrate_tokens_available?`** - Use this to check before expensive operations
266
+ - **`consume_lightrate_token_for_current_path`** - Use this when you want to consume a token for the current request path
267
+ - **`consume_lightrate_token_for_path`** - Use this for specific paths different from the current request
268
+
269
+ ### Exception Handling
270
+
271
+ The gem automatically handles rate limiting through controller callbacks:
272
+
273
+ - **HTML requests**: Redirects with flash message
274
+ - **JSON requests**: Returns 429 status with JSON error
275
+ - **XML requests**: Returns 429 status with XML error
276
+
277
+ #### Customizing Throttling Responses
278
+
279
+ You can customize the throttling response by overriding the `handle_rate_limit_exceeded` method in your controllers:
280
+
281
+ ```ruby
282
+ class ApiController < ApplicationController
283
+ throttle_with_lightrate
284
+
285
+ private
286
+
287
+ def handle_rate_limit_exceeded(exception)
288
+ # Custom throttling response
289
+ render json: {
290
+ error: 'Rate Limited',
291
+ message: 'You have exceeded the rate limit for this endpoint.',
292
+ path: exception.path,
293
+ user: exception.user_identifier,
294
+ retry_after: 30
295
+ }, status: 429
296
+ end
297
+ end
298
+ ```
299
+
300
+ ## Exception Classes
301
+
302
+ ### LightRateNoTokensAvailable
303
+
304
+ Raised when no tokens are available for a request.
305
+
306
+ ```ruby
307
+ begin
308
+ consume_lightrate_tokens(operation: 'my_operation')
309
+ rescue LightrateRails::LightRateNoTokensAvailable => e
310
+ puts "No tokens available for path: #{e.path}"
311
+ puts "User: #{e.user_identifier}"
312
+ puts "Response: #{e.response}"
313
+ end
314
+ ```
315
+
316
+ ## Configuration Options
317
+
318
+ | Option | Type | Default | Description |
319
+ |--------|------|---------|-------------|
320
+ | `api_key` | String | `nil` | **Required** - Your LightRate API key |
321
+ | `application_id` | String | `nil` | **Required** - Your LightRate Application ID |
322
+ | `enabled` | Boolean | `true` | Enable/disable throttling globally |
323
+ | `default_local_bucket_size` | Integer | `5` | Default size for local token buckets |
324
+ | `timeout` | Integer | `30` | API request timeout in seconds |
325
+ | `retry_attempts` | Integer | `3` | Number of API retry attempts |
326
+ | `logger` | Logger | `Rails.logger` | Logger for API requests |
327
+
328
+ **Note:** User identification is now configured per-controller using the `user_identifier` option in `throttle_with_lightrate`. See [Custom User Identification](#custom-user-identification) for details.
329
+
330
+ ## Development
331
+
332
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
333
+
334
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
335
+
336
+ ## Contributing
337
+
338
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lightbourne-technologies/lightrate-rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/lightbourne-technologies/lightrate-rails/blob/main/CODE_OF_CONDUCT.md).
339
+
340
+ ## License
341
+
342
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
343
+
344
+ ## Code of Conduct
345
+
346
+ Everyone interacting in the Lightrate Rails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/lightbourne-technologies/lightrate-rails/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightrateRails
4
+ class Configuration
5
+ attr_accessor :api_key, :application_id, :timeout, :retry_attempts, :logger, :default_local_bucket_size,
6
+ :enabled
7
+
8
+ def initialize
9
+ @enabled = true
10
+ @timeout = 30
11
+ @retry_attempts = 3
12
+ @logger = nil
13
+ @default_local_bucket_size = 5
14
+ end
15
+
16
+ def valid?
17
+ api_key.present? && application_id.present?
18
+ end
19
+
20
+ def to_h
21
+ {
22
+ api_key: api_key.present? ? "******" : nil,
23
+ application_id: application_id,
24
+ enabled: enabled,
25
+ timeout: timeout,
26
+ retry_attempts: retry_attempts,
27
+ logger: logger,
28
+ default_local_bucket_size: default_local_bucket_size
29
+ }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightrateRails
4
+ module ControllerHelper
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ rescue_from LightrateRails::LightRateNoTokensAvailable, with: :handle_rate_limit_exceeded
9
+
10
+ # Class-level configuration for throttling
11
+ class_attribute :lightrate_throttled_actions, default: []
12
+ class_attribute :lightrate_throttle_only_specified, default: false
13
+ class_attribute :lightrate_user_identifier_method, default: nil
14
+ class_attribute :lightrate_enabled, default: false
15
+
16
+ # Main before_action that handles token consumption
17
+ before_action :consume_lightrate_token, if: :should_throttle_current_action?
18
+ end
19
+
20
+ class_methods do
21
+ # Enable throttling for this controller
22
+ # @param options [Hash] Configuration options
23
+ # @option options [Array<Symbol, String>] :only List of action names to throttle (optional)
24
+ # @option options [Array<Symbol, String>] :except List of action names to exclude from throttling (optional)
25
+ # @option options [Proc, Symbol] :user_identifier Custom method to get user identifier (optional)
26
+ # Can be a Proc that receives the controller instance, or a Symbol for a controller method
27
+ # @example
28
+ # throttle_with_lightrate only: [:create, :update]
29
+ # throttle_with_lightrate user_identifier: -> { current_api_user.token }
30
+ # throttle_with_lightrate user_identifier: :get_user_id
31
+ def throttle_with_lightrate(options = {})
32
+ self.lightrate_enabled = true
33
+
34
+ if options.key?(:only)
35
+ self.lightrate_throttled_actions = Array(options[:only]).map(&:to_s)
36
+ self.lightrate_throttle_only_specified = true
37
+ elsif options.key?(:except)
38
+ # Store exceptions and handle in should_throttle_action?
39
+ self.lightrate_throttled_actions = Array(options[:except]).map(&:to_s)
40
+ self.lightrate_throttle_only_specified = :except
41
+ else
42
+ self.lightrate_throttled_actions = []
43
+ self.lightrate_throttle_only_specified = false
44
+ end
45
+
46
+ if options.key?(:user_identifier)
47
+ self.lightrate_user_identifier_method = options[:user_identifier]
48
+ end
49
+ end
50
+
51
+ # Disable throttling for this controller
52
+ def skip_lightrate_throttling
53
+ self.lightrate_enabled = false
54
+ self.lightrate_throttled_actions = []
55
+ self.lightrate_throttle_only_specified = true
56
+ end
57
+
58
+ # Check if the given action should be throttled
59
+ # @param action_name [String] The action name to check
60
+ # @return [Boolean] true if the action should be throttled
61
+ def should_throttle_action?(action_name)
62
+ return false unless lightrate_enabled
63
+ return false if lightrate_throttle_only_specified == true && lightrate_throttled_actions.empty?
64
+ return true if lightrate_throttled_actions.empty? && lightrate_throttle_only_specified == false
65
+
66
+ if lightrate_throttle_only_specified == :except
67
+ # If using :except, throttle everything EXCEPT the listed actions
68
+ !lightrate_throttled_actions.include?(action_name.to_s)
69
+ else
70
+ # If using :only, throttle only the listed actions
71
+ lightrate_throttled_actions.include?(action_name.to_s)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Check if tokens are available for the current request path
77
+ # Note: This method actually consumes a token to check availability
78
+ # @param user_identifier [String, nil] User identifier (defaults to current_user.id)
79
+ # @return [Boolean] true if tokens are available, false otherwise
80
+ def lightrate_tokens_available?(user_identifier: nil)
81
+ user_id = user_identifier || current_user&.id
82
+ return false unless user_id
83
+
84
+ begin
85
+ consume_lightrate_token_for_current_path(user_identifier: user_id)
86
+ true
87
+ rescue LightrateRails::LightRateNoTokensAvailable
88
+ false
89
+ end
90
+ end
91
+
92
+ # Consume a token for the current request path
93
+ # @param user_identifier [String, nil] User identifier (defaults to current_user.id)
94
+ # @return [LightrateClient::ConsumeLocalBucketTokenResponse] The response object
95
+ # @raise [LightrateRails::LightRateNoTokensAvailable] If no tokens are available
96
+ def consume_lightrate_token_for_current_path(user_identifier: nil)
97
+ user_id = user_identifier || current_user&.id
98
+ raise ArgumentError, "User identifier is required" unless user_id
99
+
100
+ response = lightrate_client.consume_local_bucket_token(
101
+ path: request.path,
102
+ http_method: request.request_method,
103
+ user_identifier: user_id
104
+ )
105
+
106
+ unless response.success
107
+ raise LightrateRails::LightRateNoTokensAvailable.new(request.path, user_id, response)
108
+ end
109
+
110
+ response
111
+ end
112
+
113
+ # Manually consume a token from the local bucket for a specific path
114
+ # @param path [String] The API path
115
+ # @param method [String] The HTTP method (defaults to current request method)
116
+ # @param user_identifier [String, nil] User identifier (defaults to current_user.id)
117
+ def consume_lightrate_token_for_path(path:, method: nil, user_identifier: nil)
118
+ user_id = user_identifier || current_user&.id
119
+ raise ArgumentError, "User identifier is required" unless user_id
120
+
121
+ http_method = method || request.request_method
122
+
123
+ response = lightrate_client.consume_local_bucket_token(
124
+ path: path,
125
+ http_method: http_method,
126
+ user_identifier: user_id
127
+ )
128
+
129
+ unless response.success
130
+ raise LightrateRails::LightRateNoTokensAvailable.new(path, user_id, response)
131
+ end
132
+
133
+ response
134
+ end
135
+
136
+ private
137
+
138
+ # Check if the current action should be throttled
139
+ # @return [Boolean]
140
+ def should_throttle_current_action?
141
+ return false unless LightrateRails.configuration.enabled
142
+ self.class.should_throttle_action?(action_name)
143
+ end
144
+
145
+ # Main before_action that consumes a token for the current request
146
+ # Raises LightRateNoTokensAvailable if no tokens are available, which is caught by rescue_from
147
+ def consume_lightrate_token
148
+ user_id = get_lightrate_user_identifier
149
+
150
+ # Skip throttling if we can't identify the user
151
+ return if user_id.nil?
152
+
153
+ begin
154
+ response = lightrate_client.consume_local_bucket_token(
155
+ path: request.path,
156
+ http_method: request.request_method,
157
+ user_identifier: user_id
158
+ )
159
+
160
+ unless response.success
161
+ raise LightrateRails::LightRateNoTokensAvailable.new(request.path, user_id, response)
162
+ end
163
+ rescue LightrateClient::APIError => e
164
+ # Handle API errors - log and continue with request
165
+ Rails.logger.warn("LightRate API error: #{e.message}") if Rails.logger
166
+ rescue LightrateClient::ConfigurationError => e
167
+ # Handle configuration errors - log and continue with request
168
+ Rails.logger.error("LightRate configuration error: #{e.message}") if Rails.logger
169
+ end
170
+ end
171
+
172
+ # Get the user identifier for the current request
173
+ # Uses controller-level configuration if available, otherwise falls back to current_user.id
174
+ # @return [String, nil]
175
+ def get_lightrate_user_identifier
176
+ if self.class.lightrate_user_identifier_method
177
+ case self.class.lightrate_user_identifier_method
178
+ when Proc
179
+ instance_exec(&self.class.lightrate_user_identifier_method)
180
+ when Symbol
181
+ send(self.class.lightrate_user_identifier_method)
182
+ else
183
+ self.class.lightrate_user_identifier_method
184
+ end
185
+ elsif respond_to?(:current_user)
186
+ current_user&.id
187
+ else
188
+ 'Anonymous'
189
+ end
190
+ rescue StandardError => e
191
+ Rails.logger.warn("Failed to get user identifier: #{e.message}") if Rails.logger
192
+ nil
193
+ end
194
+
195
+ def handle_rate_limit_exceeded(exception)
196
+ # Check if we're in an API controller (ActionController::API) or regular controller (ActionController::Base)
197
+ if self.class.ancestors.include?(ActionController::API)
198
+ # API controller - just render JSON
199
+ render json: {
200
+ error: 'Too Many Requests',
201
+ message: 'Rate limit exceeded. Please try again later.',
202
+ }, status: 429
203
+ else
204
+ # Regular controller - use respond_to
205
+ respond_to do |format|
206
+ format.html do
207
+ flash[:error] = "Rate limit exceeded. Please try again later."
208
+ redirect_back(fallback_location: root_path)
209
+ end
210
+ format.json do
211
+ render json: {
212
+ error: 'Too Many Requests',
213
+ message: 'Rate limit exceeded. Please try again later.',
214
+ }, status: 429
215
+ end
216
+ format.xml do
217
+ render xml: {
218
+ error: 'Too Many Requests',
219
+ message: 'Rate limit exceeded. Please try again later.',
220
+ }.to_xml(root: 'error'), status: 429
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ def lightrate_client
227
+ LightrateRails.client
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightrateRails
4
+ class Engine < ::Rails::Engine
5
+ config.to_prepare do
6
+ # Include in ActionController::Base
7
+ if defined?(ActionController::Base)
8
+ ActionController::Base.include LightrateRails::ControllerHelper
9
+ end
10
+
11
+ # Include in ActionController::API
12
+ if defined?(ActionController::API)
13
+ ActionController::API.include LightrateRails::ControllerHelper
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightrateRails
4
+ class Error < StandardError; end
5
+
6
+ class ConfigurationError < Error; end
7
+
8
+ class LightRateNoTokensAvailable < Error
9
+ attr_reader :path, :user_identifier, :response
10
+
11
+ def initialize(path = nil, user_identifier = nil, response = nil)
12
+ @path = path
13
+ @user_identifier = user_identifier
14
+ @response = response
15
+
16
+ message = "No tokens available for request"
17
+ message += " to #{path}" if path
18
+ message += " for user #{user_identifier}" if user_identifier
19
+
20
+ super(message)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightrateRails
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lightrate_client"
4
+ require "lightrate_rails/version"
5
+ require "lightrate_rails/engine"
6
+ require "lightrate_rails/errors"
7
+ require "lightrate_rails/configuration"
8
+ require "lightrate_rails/controller_helper"
9
+
10
+ module LightrateRails
11
+ class << self
12
+ def configure
13
+ yield(configuration)
14
+ end
15
+
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def client
21
+ @client ||= create_client
22
+ end
23
+
24
+ def create_client
25
+ raise ConfigurationError, "API key is required" unless configuration.api_key
26
+ raise ConfigurationError, "Application ID is required" unless configuration.application_id
27
+
28
+ @client = LightrateClient::Client.new(
29
+ configuration.api_key,
30
+ configuration.application_id,
31
+ {
32
+ timeout: configuration.timeout,
33
+ retry_attempts: configuration.retry_attempts,
34
+ logger: configuration.logger,
35
+ default_local_bucket_size: configuration.default_local_bucket_size
36
+ }
37
+ )
38
+ end
39
+
40
+ def reset!
41
+ @configuration = nil
42
+ @client = nil
43
+ end
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,211 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lightrate-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Lightbourne Technologies
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-10-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: lightrate-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '6.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '6.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: simplecov
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.21'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.21'
153
+ - !ruby/object:Gem::Dependency
154
+ name: vcr
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '6.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '6.0'
167
+ description: A Rails gem that provides seamless integration with Lightrate API throttling
168
+ using local token buckets and controller-level rate limiting.
169
+ email:
170
+ - grayden@lightbournetechnologies.ca
171
+ executables: []
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - Gemfile
176
+ - Gemfile.lock
177
+ - README.md
178
+ - Rakefile
179
+ - lib/lightrate_rails.rb
180
+ - lib/lightrate_rails/configuration.rb
181
+ - lib/lightrate_rails/controller_helper.rb
182
+ - lib/lightrate_rails/engine.rb
183
+ - lib/lightrate_rails/errors.rb
184
+ - lib/lightrate_rails/version.rb
185
+ homepage: https://github.com/lightbourne-technologies/lightrate-client-rails
186
+ licenses:
187
+ - MIT
188
+ metadata:
189
+ homepage_uri: https://github.com/lightbourne-technologies/lightrate-client-rails
190
+ source_code_uri: https://github.com/lightbourne-technologies/lightrate-client-rails
191
+ changelog_uri: https://github.com/lightbourne-technologies/lightrate-client-rails/blob/main/CHANGELOG.md
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: 2.7.0
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubygems_version: 3.5.21
208
+ signing_key:
209
+ specification_version: 4
210
+ summary: Ruby on Rails integration for Lightrate throttling
211
+ test_files: []