reactive-actions 0.1.0.pre.alpha.1 β†’ 0.1.0.pre.alpha.3

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.
data/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # ReactiveActions
2
2
 
3
- ReactiveActions is a Rails gem that provides a framework for handling reactive actions in your Rails application.
3
+ ReactiveActions is a Rails gem that provides a framework for handling reactive actions in your Rails application with Stimulus-style DOM binding support.
4
4
 
5
5
  ## 🚧 Status
6
6
 
7
- This gem is currently in alpha (0.1.0-alpha.1). The API may change between versions.
7
+ This gem is currently in alpha (0.1.0-alpha.3). The API may change between versions.
8
8
 
9
9
  ## πŸ“¦ Installation
10
10
 
11
11
  Add this line to your application's Gemfile:
12
12
 
13
13
  ```ruby
14
- gem 'reactive-actions', '0.1.0-alpha.1'
14
+ gem 'reactive-actions', '0.1.0-alpha.3'
15
15
  ```
16
16
 
17
17
  And then execute:
@@ -64,6 +64,17 @@ Add ReactiveActions JavaScript client? (y/n) y
64
64
  βœ“ Added JavaScript client to importmap
65
65
  βœ“ Added ReactiveActions import to app/javascript/application.js
66
66
 
67
+ Configure rate limiting? (optional but recommended for production) (y/n) y
68
+ Enable rate limiting features? (y/n) y
69
+ Enable global controller-level rate limiting? (recommended) (y/n) y
70
+ Global rate limit (requests per window): [600] 1000
71
+ Global rate limit window: [1.minute] 5.minutes
72
+ Configure custom rate limit key generator? (advanced) (y/n) n
73
+ βœ“ Rate limiting configured:
74
+ - Rate limiting: ENABLED
75
+ - Global rate limiting: ENABLED
76
+ - Global limit: 1000 requests per 5.minutes
77
+
67
78
  Configure advanced options? (y/n) n
68
79
 
69
80
  ================================================================
@@ -85,18 +96,37 @@ $ rails generate reactive_actions:install --mount-path=/api/reactive
85
96
  # Skip example action generation
86
97
  $ rails generate reactive_actions:install --skip-example
87
98
 
99
+ # Enable rate limiting during installation
100
+ $ rails generate reactive_actions:install --enable-rate-limiting --enable-global-rate-limiting
101
+
102
+ # Configure rate limiting with custom limits
103
+ $ rails generate reactive_actions:install --enable-rate-limiting --global-rate-limit=1000 --global-rate-limit-window="5.minutes"
104
+
88
105
  # Quiet installation with defaults
89
106
  $ rails generate reactive_actions:install --quiet
90
107
  ```
91
108
 
92
109
  ### Available Options
93
110
 
111
+ **Basic Options:**
94
112
  - `--skip-routes` - Skip adding routes to your application
95
113
  - `--skip-javascript` - Skip adding JavaScript imports and setup
96
114
  - `--skip-example` - Skip generating the example action file
97
115
  - `--mount-path=PATH` - Specify custom mount path (default: `/reactive_actions`)
98
116
  - `--quiet` - Run installation with minimal output and default settings
99
117
 
118
+ **JavaScript Client Options:**
119
+ - `--auto-initialize` - Auto-initialize ReactiveActions on page load (default: true)
120
+ - `--enable-dom-binding` - Enable automatic DOM binding (default: true)
121
+ - `--enable-mutation-observer` - Enable mutation observer for dynamic content (default: true)
122
+ - `--default-http-method=METHOD` - Default HTTP method for actions (default: 'POST')
123
+
124
+ **Rate Limiting Options:**
125
+ - `--enable-rate-limiting` - Enable rate limiting features
126
+ - `--enable-global-rate-limiting` - Enable global controller-level rate limiting
127
+ - `--global-rate-limit=NUMBER` - Global rate limit (requests per window, default: 600)
128
+ - `--global-rate-limit-window=DURATION` - Global rate limit window (default: '1.minute')
129
+
100
130
  ### What Gets Installed
101
131
 
102
132
  The generator will:
@@ -106,11 +136,12 @@ The generator will:
106
136
  - βœ… Add JavaScript to your `config/importmap.rb` (Rails 8 native)
107
137
  - βœ… Automatically import ReactiveActions in your `application.js`
108
138
  - βœ… Create an initializer file with configuration options
139
+ - βœ… Configure rate limiting settings (if enabled)
109
140
  - βœ… Optionally configure advanced settings like custom delegated methods
110
141
 
111
142
  ## ⚑ Rails 8 Native JavaScript Integration
112
143
 
113
- ReactiveActions now uses Rails 8's native JavaScript approach with **Importmap + Propshaft**, providing seamless integration without additional build steps.
144
+ ReactiveActions uses Rails 8's native JavaScript approach with **Importmap + Propshaft**, providing seamless integration without additional build steps.
114
145
 
115
146
  ### Automatic Setup
116
147
 
@@ -145,21 +176,43 @@ The JavaScript client supports both Rails 8 (Importmap) and older setups (Sprock
145
176
 
146
177
  ## πŸš€ Usage
147
178
 
148
- ### HTTP API
179
+ ### DOM Binding (Recommended)
149
180
 
150
- Once installed, you can access the reactive actions by sending requests to your configured endpoint:
181
+ The easiest way to use ReactiveActions is with DOM binding - no JavaScript required:
151
182
 
183
+ ```html
184
+ <!-- Basic button click -->
185
+ <button reactive-action="click->update_user"
186
+ reactive-action-user-id="123">
187
+ Update User
188
+ </button>
189
+
190
+ <!-- Live search input -->
191
+ <input reactive-action="input->search_users"
192
+ reactive-action-live="true"
193
+ placeholder="Search...">
194
+
195
+ <!-- Form submission -->
196
+ <form reactive-action="submit->create_post">
197
+ <input name="title" type="text" required>
198
+ <button type="submit">Create Post</button>
199
+ </form>
200
+
201
+ <!-- RESTful actions with HTTP methods -->
202
+ <button reactive-action="click->post#create_user">Create</button>
203
+ <button reactive-action="click->put#update_user">Update</button>
204
+ <button reactive-action="click->delete#delete_user">Delete</button>
152
205
  ```
153
- GET/POST/PUT/PATCH/DELETE /reactive_actions/execute
154
- ```
155
206
 
156
- Or if you used a custom mount path:
207
+ ### HTTP API
208
+
209
+ You can also access reactive actions by sending direct HTTP requests:
157
210
 
158
211
  ```
159
- GET/POST/PUT/PATCH/DELETE /your-custom-path/execute
212
+ GET/POST/PUT/PATCH/DELETE /reactive_actions/execute
160
213
  ```
161
214
 
162
- You can pass parameters:
215
+ Parameters:
163
216
  - `action_name`: The name of the action to execute
164
217
  - `action_params`: Parameters for the action
165
218
 
@@ -173,35 +226,245 @@ response = Net::HTTP.post(
173
226
  )
174
227
  ```
175
228
 
176
- ### Creating Custom Actions
229
+ ### JavaScript Client
230
+
231
+ For programmatic access, use the JavaScript client:
232
+
233
+ ```javascript
234
+ // Basic usage (POST method by default)
235
+ ReactiveActions.execute('update_user', { id: 1, name: 'New Name' })
236
+ .then(response => {
237
+ if (response.ok) {
238
+ console.log('Success:', response);
239
+ } else {
240
+ console.error('Error:', response);
241
+ }
242
+ });
243
+
244
+ // Using specific HTTP methods
245
+ ReactiveActions.get('fetch_user', { id: 1 });
246
+ ReactiveActions.post('create_user', { name: 'New User' });
247
+ ReactiveActions.put('update_user', { id: 1, name: 'Updated Name' });
248
+ ReactiveActions.patch('partial_update', { id: 1, status: 'active' });
249
+ ReactiveActions.delete('delete_user', { id: 1 });
250
+ ```
251
+
252
+ ## πŸ“ DOM Binding Reference
253
+
254
+ ### Action Syntax
255
+
256
+ Use `reactive-action` with the format `event->action_name` or `event->method#action_name`:
257
+
258
+ ```html
259
+ <!-- Basic actions (uses default POST method) -->
260
+ <button reactive-action="click->update_user">Update User</button>
261
+ <input reactive-action="change->search_users" type="text">
262
+ <div reactive-action="hover->show_preview">Hover me</div>
263
+
264
+ <!-- With HTTP methods -->
265
+ <button reactive-action="click->put#update_user">Update User (PUT)</button>
266
+ <button reactive-action="click->delete#delete_user">Delete User</button>
267
+ <button reactive-action="click->get#fetch_user">Fetch User</button>
268
+
269
+ <!-- Multiple actions -->
270
+ <button reactive-action="click->post#save mouseenter->get#preview">
271
+ Save Item
272
+ </button>
273
+ ```
274
+
275
+ ### Passing Data
276
+
277
+ Use `reactive-action-*` attributes to pass data:
278
+
279
+ ```html
280
+ <button reactive-action="click->update_user"
281
+ reactive-action-user-id="123"
282
+ reactive-action-name="John Doe">
283
+ Update User
284
+ </button>
285
+ ```
286
+
287
+ Data attributes are automatically converted from kebab-case to snake_case:
288
+ - `reactive-action-user-id="123"` β†’ `{ user_id: "123" }`
289
+ - `reactive-action-first-name="John"` β†’ `{ first_name: "John" }`
290
+
291
+ ### Supported Events
292
+
293
+ - **`click`** - Mouse clicks
294
+ - **`hover`** - Mouse hover (mouseenter)
295
+ - **`change`** - Input value changes
296
+ - **`input`** - Input value changes (live)
297
+ - **`submit`** - Form submissions
298
+ - **`focus`** - Element receives focus
299
+ - **`blur`** - Element loses focus
300
+ - **`mouseenter`/`mouseleave`** - Mouse interactions
301
+ - **`keyup`/`keydown`** - Keyboard events
302
+
303
+ ### Loading States
304
+
305
+ Elements automatically get loading states:
306
+
307
+ ```css
308
+ .reactive-loading {
309
+ opacity: 0.6;
310
+ cursor: not-allowed;
311
+ }
312
+
313
+ /* Buttons get disabled and show loading text */
314
+ button.reactive-loading {
315
+ background-color: #ccc;
316
+ }
317
+ ```
318
+
319
+ Custom loading text:
320
+ ```html
321
+ <button reactive-action="click->slow_action"
322
+ data-loading-text="Processing...">
323
+ Start Process
324
+ </button>
325
+ ```
326
+
327
+ ### Success and Error Handling
328
+
329
+ #### Custom Events
330
+ ```javascript
331
+ // Listen for successful actions
332
+ document.addEventListener('reactive-action:success', (event) => {
333
+ const { response, element, originalEvent } = event.detail;
334
+ console.log('Action succeeded:', response);
335
+ });
336
+
337
+ // Listen for action errors
338
+ document.addEventListener('reactive-action:error', (event) => {
339
+ const { error, element, originalEvent } = event.detail;
340
+ console.error('Action failed:', error);
341
+ });
342
+ ```
343
+
344
+ #### Callback Functions
345
+ ```html
346
+ <button reactive-action="click->update_user"
347
+ reactive-action-success="handleSuccess"
348
+ reactive-action-error="handleError">
349
+ Update User
350
+ </button>
351
+
352
+ <script>
353
+ function handleSuccess(response, element, event) {
354
+ alert('User updated successfully!');
355
+ }
356
+
357
+ function handleError(error, element, event) {
358
+ alert('Failed to update user: ' + error.message);
359
+ }
360
+ </script>
361
+ ```
362
+
363
+ ## βš™οΈ Configuration
364
+
365
+ ReactiveActions provides flexible initialization options:
366
+
367
+ ### Automatic Initialization (Default)
368
+
369
+ ```javascript
370
+ // Automatically set up during installation
371
+ // Available globally as window.ReactiveActions
372
+ ReactiveActions.execute('action_name', { param: 'value' })
373
+ ```
177
374
 
178
- You can create custom actions by inheriting from `ReactiveActions::ReactiveAction`:
375
+ ### Manual Initialization
376
+
377
+ ```javascript
378
+ // Import the client class
379
+ import ReactiveActionsClient from "reactive_actions"
380
+
381
+ // Create and configure instance
382
+ const reactiveActions = new ReactiveActionsClient({
383
+ baseUrl: '/custom/path/execute',
384
+ enableAutoBinding: true,
385
+ enableMutationObserver: true,
386
+ defaultHttpMethod: 'POST'
387
+ });
388
+
389
+ // Initialize DOM bindings
390
+ reactiveActions.initialize();
391
+
392
+ // Make available globally (optional)
393
+ window.ReactiveActions = reactiveActions;
394
+ ```
395
+
396
+ ### Configuration Options
397
+
398
+ | Option | Default | Description |
399
+ |--------|---------|-------------|
400
+ | `baseUrl` | `'/reactive_actions/execute'` | API endpoint for action requests |
401
+ | `enableAutoBinding` | `true` | Automatically bind elements on initialization |
402
+ | `enableMutationObserver` | `true` | Watch for dynamically added elements |
403
+ | `defaultHttpMethod` | `'POST'` | Default HTTP method when not specified |
404
+
405
+ ### Advanced Configuration Examples
406
+
407
+ ```javascript
408
+ // Environment-specific configuration
409
+ const reactiveActions = new ReactiveActionsClient({
410
+ baseUrl: Rails.env === 'development' ?
411
+ 'http://localhost:3000/reactive_actions/execute' :
412
+ '/reactive_actions/execute'
413
+ });
414
+
415
+ // For SPAs with manual DOM control
416
+ const manualReactiveActions = new ReactiveActionsClient({
417
+ enableAutoBinding: false,
418
+ enableMutationObserver: false
419
+ });
420
+
421
+ // Initialize only when needed
422
+ document.addEventListener('turbo:load', () => {
423
+ manualReactiveActions.initialize();
424
+ });
425
+
426
+ // Reconfigure after creation
427
+ reactiveActions.configure({
428
+ defaultHttpMethod: 'PUT',
429
+ enableAutoBinding: false
430
+ }).reinitialize();
431
+
432
+ // Get current configuration
433
+ console.log(reactiveActions.getConfig());
434
+
435
+ // Bind specific elements manually
436
+ reactiveActions.bindElement(document.getElementById('my-button'));
437
+
438
+ // Force re-initialization
439
+ reactiveActions.reinitialize();
440
+ ```
441
+
442
+ ## 🎯 Creating Custom Actions
443
+
444
+ Create custom actions by inheriting from `ReactiveActions::ReactiveAction`:
179
445
 
180
446
  ```ruby
181
447
  # app/reactive_actions/update_user_action.rb
182
448
  class UpdateUserAction < ReactiveActions::ReactiveAction
183
449
  def action
184
- user = User.find(action_params[:id])
185
- user.update(action_params[:user_attributes])
450
+ user = User.find(action_params[:user_id])
451
+ user.update(name: action_params[:name])
186
452
 
187
453
  @result = {
188
454
  success: true,
189
- user: user.as_json
455
+ user: user.as_json(only: [:id, :name, :email])
190
456
  }
191
457
  end
192
458
 
193
459
  def response
194
- render json: {
195
- success: true,
196
- data: @result
197
- }
460
+ render json: @result
198
461
  end
199
462
  end
200
463
  ```
201
464
 
202
465
  ### Action Directory Structure
203
466
 
204
- Actions are placed in the `app/reactive_actions` directory structure:
467
+ Actions are placed in the `app/reactive_actions` directory:
205
468
 
206
469
  ```
207
470
  app/
@@ -216,82 +479,87 @@ app/
216
479
  β”‚ └── update_product_action.rb
217
480
  ```
218
481
 
219
- Actions in subdirectories are automatically loaded and namespaced under `ReactiveActions`. For example, a file at `app/reactive_actions/user_actions/create_user_action.rb` becomes accessible as `ReactiveActions::CreateUserAction`.
220
-
221
482
  ### Action Naming Convention
222
483
 
223
- Action files should follow the naming convention:
224
484
  - File name: `snake_case_action.rb` (e.g., `update_user_action.rb`)
225
485
  - Class name: `CamelCaseAction` (e.g., `UpdateUserAction`)
226
- - HTTP parameter: `snake_case` without the `_action` suffix (e.g., `update_user`)
486
+ - HTTP parameter: `snake_case` without `_action` suffix (e.g., `update_user`)
227
487
 
228
- Examples:
229
- - `create_user_action.rb` β†’ `CreateUserAction` β†’ called with `action_name: "create_user"`
230
- - `fetch_product_action.rb` β†’ `FetchProductAction` β†’ called with `action_name: "fetch_product"`
488
+ ### Advanced Action Examples
231
489
 
232
- ### Advanced Examples
490
+ #### RESTful User Management
491
+ ```ruby
492
+ # app/reactive_actions/create_user_action.rb
493
+ class CreateUserAction < ReactiveActions::ReactiveAction
494
+ def action
495
+ user = User.create!(action_params.slice(:name, :email))
496
+ @result = { user: user.as_json, message: 'User created successfully' }
497
+ end
233
498
 
234
- #### Complex Action with Validation and Error Handling
499
+ def response
500
+ render json: @result, status: :created
501
+ end
502
+ end
235
503
 
236
- ```ruby
237
- # app/reactive_actions/process_payment_action.rb
238
- class ProcessPaymentAction < ReactiveActions::ReactiveAction
504
+ # app/reactive_actions/update_user_action.rb
505
+ class UpdateUserAction < ReactiveActions::ReactiveAction
239
506
  def action
240
- # Validate required parameters
241
- validate_parameters!
242
-
243
- # Process the payment
244
- payment_service = PaymentService.new(
245
- amount: action_params[:amount],
246
- currency: action_params[:currency],
247
- payment_method: action_params[:payment_method]
248
- )
249
-
250
- @result = payment_service.process
251
-
252
- # Log the transaction
253
- PaymentLog.create(
254
- amount: action_params[:amount],
255
- status: @result[:status],
256
- transaction_id: @result[:transaction_id]
257
- )
258
- rescue PaymentError => e
259
- @error = { type: 'payment_failed', message: e.message }
260
- rescue ValidationError => e
261
- @error = { type: 'validation_failed', message: e.message }
507
+ user = User.find(action_params[:user_id])
508
+ user.update!(action_params.slice(:name, :email))
509
+ @result = { user: user.as_json, message: 'User updated successfully' }
262
510
  end
263
511
 
264
512
  def response
265
- if @error
266
- render json: { success: false, error: @error }, status: :unprocessable_entity
267
- else
268
- render json: { success: true, data: @result }
269
- end
513
+ render json: @result
270
514
  end
515
+ end
271
516
 
272
- private
517
+ # app/reactive_actions/delete_user_action.rb
518
+ class DeleteUserAction < ReactiveActions::ReactiveAction
519
+ def action
520
+ user = User.find(action_params[:user_id])
521
+ user.destroy!
522
+ @result = { message: 'User deleted successfully' }
523
+ end
273
524
 
274
- def validate_parameters!
275
- required_params = %i[amount currency payment_method]
276
- missing_params = required_params.select { |param| action_params[param].blank? }
277
-
278
- raise ValidationError, "Missing parameters: #{missing_params.join(', ')}" if missing_params.any?
279
- raise ValidationError, "Invalid amount" unless action_params[:amount].to_f > 0
525
+ def response
526
+ render json: @result
280
527
  end
281
528
  end
282
529
  ```
283
530
 
284
- #### Action with Background Job Integration
531
+ #### Live Search with Filtering
532
+ ```ruby
533
+ # app/reactive_actions/search_users_action.rb
534
+ class SearchUsersAction < ReactiveActions::ReactiveAction
535
+ def action
536
+ query = action_params[:value] || action_params[:query]
537
+
538
+ users = User.where("name ILIKE ? OR email ILIKE ?", "%#{query}%", "%#{query}%")
539
+ .limit(10)
540
+ .select(:id, :name, :email)
541
+
542
+ @result = {
543
+ users: users.as_json,
544
+ count: users.count,
545
+ query: query
546
+ }
547
+ end
548
+
549
+ def response
550
+ render json: @result
551
+ end
552
+ end
553
+ ```
285
554
 
555
+ #### Background Job Integration
286
556
  ```ruby
287
557
  # app/reactive_actions/generate_report_action.rb
288
558
  class GenerateReportAction < ReactiveActions::ReactiveAction
289
559
  def action
290
- # Queue the report generation job
291
560
  job = ReportGenerationJob.perform_later(
292
561
  user_id: action_params[:user_id],
293
- report_type: action_params[:report_type],
294
- filters: action_params[:filters] || {}
562
+ report_type: action_params[:report_type]
295
563
  )
296
564
 
297
565
  @result = {
@@ -302,408 +570,972 @@ class GenerateReportAction < ReactiveActions::ReactiveAction
302
570
  end
303
571
 
304
572
  def response
305
- render json: {
306
- success: true,
307
- message: 'Report generation started',
308
- data: @result
309
- }
573
+ render json: @result, status: :accepted
310
574
  end
311
575
  end
312
576
  ```
313
577
 
314
- ## πŸ’» JavaScript Client
578
+ ## πŸ” Security Checks
315
579
 
316
- ReactiveActions includes a modern JavaScript client that's automatically available after installation.
580
+ ReactiveActions provides a comprehensive security system through the `SecurityChecks` module, allowing you to define custom security filters that run before your actions execute.
317
581
 
318
- ### Global Usage (Recommended)
582
+ ### Basic Security Checks
319
583
 
320
- After installation, `ReactiveActions` is globally available:
584
+ Add security checks to your actions using the `security_check` class method:
321
585
 
322
- ```javascript
323
- // Basic usage (POST method by default)
324
- ReactiveActions.execute('update_user', { id: 1, name: 'New Name' })
325
- .then(response => {
326
- if (response.ok) {
327
- console.log('Success:', response);
328
- } else {
329
- console.error('Error:', response);
330
- }
331
- });
586
+ ```ruby
587
+ # app/reactive_actions/protected_action.rb
588
+ class ProtectedAction < ReactiveActions::ReactiveAction
589
+ # Single security check
590
+ security_check :require_authentication
332
591
 
333
- // Using specific HTTP methods
334
- ReactiveActions.get('fetch_user', { id: 1 });
335
- ReactiveActions.post('create_user', { name: 'New User' });
336
- ReactiveActions.put('update_user', { id: 1, name: 'Updated Name' });
337
- ReactiveActions.patch('partial_update', { id: 1, status: 'active' });
338
- ReactiveActions.delete('delete_user', { id: 1 });
592
+ def action
593
+ @result = { message: "This action requires authentication" }
594
+ end
339
595
 
340
- // With custom options
341
- ReactiveActions.execute('custom_action', { data: 'value' }, {
342
- method: 'POST',
343
- contentType: 'application/json'
344
- });
596
+ def response
597
+ render json: @result
598
+ end
599
+
600
+ private
601
+
602
+ def require_authentication
603
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
604
+ end
605
+ end
345
606
  ```
346
607
 
347
- ### ES Module Import (Advanced)
608
+ ### Multiple Security Checks
348
609
 
349
- For more control, you can import it explicitly:
610
+ Chain multiple security checks for layered protection:
350
611
 
351
- ```javascript
352
- import ReactiveActions from "reactive_actions"
612
+ ```ruby
613
+ # app/reactive_actions/admin_action.rb
614
+ class AdminAction < ReactiveActions::ReactiveAction
615
+ # Multiple security checks run in order
616
+ security_check :require_authentication
617
+ security_check :require_admin_role
353
618
 
354
- // Use it in your module
355
- ReactiveActions.execute('action_name', { param: 'value' })
619
+ def action
620
+ @result = { message: "Admin-only action executed successfully" }
621
+ end
622
+
623
+ def response
624
+ render json: @result
625
+ end
626
+
627
+ private
628
+
629
+ def require_authentication
630
+ raise ReactiveActions::SecurityCheckError, "Please log in" unless current_user
631
+ end
632
+
633
+ def require_admin_role
634
+ raise ReactiveActions::SecurityCheckError, "Admin access required" unless current_user.admin?
635
+ end
636
+ end
356
637
  ```
357
638
 
358
- ### Client Features
639
+ ### Lambda-Based Security Checks
359
640
 
360
- The client automatically:
361
- - βœ… **Handles CSRF tokens** - Automatically includes Rails CSRF protection
362
- - βœ… **Formats requests** - Properly formats GET vs POST/PUT/PATCH/DELETE requests
363
- - βœ… **Parses responses** - Automatically parses JSON responses
364
- - βœ… **Returns promises** - Modern async/await compatible
365
- - βœ… **Error handling** - Provides structured error information
366
- - βœ… **Multiple HTTP methods** - Support for GET, POST, PUT, PATCH, DELETE
641
+ Use inline lambdas for simple or dynamic security checks:
367
642
 
368
- ## Security
643
+ ```ruby
644
+ # app/reactive_actions/ownership_action.rb
645
+ class OwnershipAction < ReactiveActions::ReactiveAction
646
+ # Inline lambda security check
647
+ security_check -> {
648
+ raise ReactiveActions::SecurityCheckError, "Must be logged in" unless current_user
649
+
650
+ if action_params[:user_id].present?
651
+ unless current_user.id.to_s == action_params[:user_id].to_s
652
+ raise ReactiveActions::SecurityCheckError, "Can only access your own data"
653
+ end
654
+ end
655
+ }
656
+
657
+ def action
658
+ @result = { message: "Ownership check passed" }
659
+ end
369
660
 
370
- ReactiveActions implements several security measures to protect your application:
661
+ def response
662
+ render json: @result
663
+ end
664
+ end
665
+ ```
371
666
 
372
- ### πŸ”’ **Built-in Security Features**
667
+ ### Conditional Security Checks
373
668
 
374
- #### Parameter Sanitization
375
- - **Input validation**: Action names are validated against safe patterns (`/\A[a-zA-Z_][a-zA-Z0-9_]*\z/`)
376
- - **Parameter key sanitization**: Only alphanumeric characters, underscores, and hyphens allowed
377
- - **String length limits**: Prevents memory exhaustion attacks (max 10,000 chars)
378
- - **Dangerous prefix filtering**: Blocks parameters starting with `__`, `eval`, `exec`, `system`, etc.
669
+ Apply security checks conditionally using `:if`, `:unless`, `:only`, or `:except`:
379
670
 
380
- #### CSRF Protection
381
- - **Automatic CSRF tokens**: JavaScript client automatically includes Rails CSRF tokens
382
- - **Same-origin requests**: Credentials are sent only to same-origin requests
383
- - **Controller integration**: Inherits from `ActionController::Base` with CSRF protection
671
+ ```ruby
672
+ # app/reactive_actions/conditional_action.rb
673
+ class ConditionalAction < ReactiveActions::ReactiveAction
674
+ # Always require authentication
675
+ security_check :require_authentication
676
+
677
+ # Only require special access if special mode is enabled
678
+ security_check :require_special_access, if: -> { action_params[:special_mode] == "true" }
679
+
680
+ # Skip ownership check for admin users
681
+ security_check :require_ownership, unless: -> { current_user&.admin? }
384
682
 
385
- #### Code Injection Prevention
386
- - **Class name validation**: Action names are sanitized before constant lookup
387
- - **Namespace isolation**: Actions are properly namespaced to prevent conflicts
388
- - **Parameter filtering**: Recursive parameter sanitization for nested structures
683
+ def action
684
+ @result = { message: "Conditional security checks passed" }
685
+ end
389
686
 
390
- ### πŸ›‘οΈ **Security Best Practices**
687
+ def response
688
+ render json: @result
689
+ end
690
+
691
+ private
692
+
693
+ def require_authentication
694
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
695
+ end
696
+
697
+ def require_special_access
698
+ unless current_user.special_access?
699
+ raise ReactiveActions::SecurityCheckError, "Special access required"
700
+ end
701
+ end
702
+
703
+ def require_ownership
704
+ resource_id = action_params[:resource_id]
705
+ resource = current_user.resources.find_by(id: resource_id)
706
+ raise ReactiveActions::SecurityCheckError, "Resource not found" unless resource
707
+ end
708
+ end
709
+ ```
710
+
711
+ ### Skipping Security Checks
712
+
713
+ For public actions that don't need any security checks:
391
714
 
392
715
  ```ruby
393
- # app/reactive_actions/secure_action.rb
394
- class SecureAction < ReactiveActions::ReactiveAction
716
+ # app/reactive_actions/public_action.rb
717
+ class PublicAction < ReactiveActions::ReactiveAction
718
+ # Skip all security checks for this action
719
+ skip_security_checks
720
+
395
721
  def action
396
- # Always validate user permissions
397
- raise ReactiveActions::UnauthorizedError unless current_user&.admin?
722
+ @result = { message: "This is a public action" }
723
+ end
724
+
725
+ def response
726
+ render json: @result
727
+ end
728
+ end
729
+ ```
730
+
731
+ ### Security Check Options
732
+
733
+ The `security_check` method supports several options for fine-grained control:
734
+
735
+ ```ruby
736
+ class ExampleAction < ReactiveActions::ReactiveAction
737
+ # Run only for specific actions (if you have multiple action methods)
738
+ security_check :check_method, only: [:create, :update]
739
+
740
+ # Skip for specific actions
741
+ security_check :check_method, except: [:index, :show]
742
+
743
+ # Conditional execution
744
+ security_check :check_method, if: :some_condition?
745
+ security_check :check_method, unless: :some_other_condition?
746
+
747
+ # Combine conditions
748
+ security_check :check_method, if: -> { params[:secure] == "true" }, unless: :development_mode?
749
+
750
+ private
751
+
752
+ def check_method
753
+ # Your security logic here
754
+ end
755
+
756
+ def some_condition?
757
+ # Your condition logic
758
+ end
759
+
760
+ def development_mode?
761
+ Rails.env.development?
762
+ end
763
+ end
764
+ ```
765
+
766
+ ### Security Error Handling
767
+
768
+ Security checks raise `ReactiveActions::SecurityCheckError` when they fail. This error is automatically caught and returned as a proper HTTP response:
769
+
770
+ ```json
771
+ {
772
+ "success": false,
773
+ "error": {
774
+ "type": "SecurityCheckError",
775
+ "message": "Authentication required",
776
+ "code": "SECURITY_CHECK_FAILED"
777
+ }
778
+ }
779
+ ```
780
+
781
+ ### Real-World Security Examples
782
+
783
+ #### User Resource Access Control
784
+ ```ruby
785
+ # app/reactive_actions/update_profile_action.rb
786
+ class UpdateProfileAction < ReactiveActions::ReactiveAction
787
+ security_check :require_authentication
788
+ security_check :verify_profile_ownership
789
+
790
+ def action
791
+ profile = current_user.profile
792
+ profile.update!(action_params.slice(:bio, :website, :location))
793
+ @result = { profile: profile.as_json }
794
+ end
795
+
796
+ def response
797
+ render json: @result
798
+ end
799
+
800
+ private
801
+
802
+ def require_authentication
803
+ raise ReactiveActions::SecurityCheckError, "Please log in" unless current_user
804
+ end
805
+
806
+ def verify_profile_ownership
807
+ profile_id = action_params[:profile_id]
808
+ return unless profile_id.present? # Skip check if no profile_id specified
398
809
 
399
- # Validate and sanitize inputs
400
- user_id = action_params[:user_id].to_i
401
- raise ReactiveActions::InvalidParametersError, "Invalid user ID" if user_id <= 0
810
+ unless current_user.profile.id.to_s == profile_id.to_s
811
+ raise ReactiveActions::SecurityCheckError, "Can only update your own profile"
812
+ end
813
+ end
814
+ end
815
+ ```
816
+
817
+ #### Role-Based Access Control
818
+ ```ruby
819
+ # app/reactive_actions/moderate_content_action.rb
820
+ class ModerateContentAction < ReactiveActions::ReactiveAction
821
+ security_check :require_authentication
822
+ security_check :require_moderator_role
823
+
824
+ def action
825
+ content = Content.find(action_params[:content_id])
826
+ content.update!(status: action_params[:status],
827
+ moderated_by: current_user.id)
828
+ @result = { content: content.as_json }
829
+ end
830
+
831
+ def response
832
+ render json: @result
833
+ end
834
+
835
+ private
836
+
837
+ def require_authentication
838
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
839
+ end
840
+
841
+ def require_moderator_role
842
+ unless current_user.moderator? || current_user.admin?
843
+ raise ReactiveActions::SecurityCheckError, "Moderator access required"
844
+ end
845
+ end
846
+ end
847
+ ```
848
+
849
+ #### API Key Validation
850
+ ```ruby
851
+ # app/reactive_actions/api_action.rb
852
+ class ApiAction < ReactiveActions::ReactiveAction
853
+ security_check :validate_api_key
854
+ security_check :check_rate_limit
855
+
856
+ def action
857
+ @result = { data: "API response data" }
858
+ end
859
+
860
+ def response
861
+ render json: @result
862
+ end
863
+
864
+ private
865
+
866
+ def validate_api_key
867
+ api_key = action_params[:api_key] || controller.request.headers['X-API-Key']
402
868
 
403
- # Use strong parameters if integrating with models
404
- permitted_params = action_params.slice(:name, :email).permit!
869
+ unless api_key.present? && ApiKey.valid?(api_key)
870
+ raise ReactiveActions::SecurityCheckError, "Invalid or missing API key"
871
+ end
405
872
 
406
- @result = User.find(user_id).update(permitted_params)
873
+ @api_key = ApiKey.find_by(key: api_key)
874
+ end
875
+
876
+ def check_rate_limit
877
+ return unless @api_key
878
+
879
+ if @api_key.rate_limit_exceeded?
880
+ raise ReactiveActions::SecurityCheckError, "Rate limit exceeded"
881
+ end
407
882
  end
408
883
  end
409
884
  ```
410
885
 
411
- ### ⚠️ **Security Considerations**
886
+ ## 🚦 Rate Limiting
412
887
 
413
- - **Always validate user permissions** in your actions
414
- - **Use Rails strong parameters** when working with model updates
415
- - **Sanitize file uploads** if handling file parameters
416
- - **Implement rate limiting** for public-facing actions
417
- - **Log security events** for audit trails
888
+ ReactiveActions provides comprehensive rate limiting functionality to protect your application from abuse and ensure fair resource usage. Rate limiting is **disabled by default** and must be explicitly enabled in your configuration.
418
889
 
419
- ## Performance
890
+ ### πŸ”§ Configuration
891
+
892
+ Rate limiting is configured in your `config/initializers/reactive_actions.rb` file:
893
+
894
+ ```ruby
895
+ ReactiveActions.configure do |config|
896
+ # Enable rate limiting functionality
897
+ config.rate_limiting_enabled = true
898
+
899
+ # Enable global controller-level rate limiting
900
+ config.global_rate_limiting_enabled = true
901
+ config.global_rate_limit = 600 # 600 requests per window
902
+ config.global_rate_limit_window = 1.minute # per minute
903
+
904
+ # Optional: Custom rate limit key generator
905
+ config.rate_limit_key_generator = ->(request, action_name) do
906
+ user_id = request.headers['X-User-ID'] || 'anonymous'
907
+ "#{action_name}:user:#{user_id}"
908
+ end
909
+ end
910
+ ```
420
911
 
421
- ### πŸš€ **Performance Characteristics**
912
+ ### Configuration Options
422
913
 
423
- ReactiveActions is designed to be lightweight and efficient:
914
+ | Option | Default | Description |
915
+ |--------|---------|-------------|
916
+ | `rate_limiting_enabled` | `false` | Master switch for all rate limiting features |
917
+ | `global_rate_limiting_enabled` | `false` | Enable controller-level rate limiting |
918
+ | `global_rate_limit` | `600` | Global rate limit (requests per window) |
919
+ | `global_rate_limit_window` | `1.minute` | Time window for global rate limiting |
920
+ | `rate_limit_key_generator` | `nil` | Custom key generator proc |
424
921
 
425
- - **Minimal overhead**: Direct controller execution without complex middleware chains
426
- - **No database dependencies**: Core functionality doesn't require database connections
427
- - **Efficient autoloading**: Actions are loaded on-demand using Rails' autoloading
428
- - **Memory efficient**: Parameter sanitization prevents memory exhaustion attacks
922
+ ### 🎯 Action-Level Rate Limiting
429
923
 
430
- ### πŸ“Š **Performance Best Practices**
924
+ Include the `RateLimiter` concern in your actions to add rate limiting functionality:
431
925
 
432
- #### Action Design
433
926
  ```ruby
434
- # βœ… Good: Lightweight action with focused responsibility
435
- class QuickUpdateAction < ReactiveActions::ReactiveAction
927
+ # app/reactive_actions/api_action.rb
928
+ class ApiAction < ReactiveActions::ReactiveAction
929
+ include ReactiveActions::Concerns::RateLimiter
930
+
436
931
  def action
437
- User.where(id: action_params[:id]).update_all(
438
- last_seen_at: Time.current
439
- )
932
+ # Basic rate limiting: 10 requests per minute per user
933
+ rate_limit!(key: "user:#{current_user&.id}", limit: 10, window: 1.minute)
934
+
935
+ @result = { data: "API response" }
936
+ end
937
+
938
+ def response
939
+ render json: @result
440
940
  end
441
941
  end
942
+ ```
943
+
944
+ ### πŸ”‘ Key-Based Rate Limiting
945
+
946
+ Rate limiting works with different key strategies:
947
+
948
+ ```ruby
949
+ class FlexibleRateLimitAction < ReactiveActions::ReactiveAction
950
+ include ReactiveActions::Concerns::RateLimiter
442
951
 
443
- # ❌ Avoid: Heavy operations that should be background jobs
444
- class SlowReportAction < ReactiveActions::ReactiveAction
445
952
  def action
446
- # This should be a background job instead
447
- @result = generate_complex_report_synchronously
953
+ case action_params[:rate_limit_type]
954
+ when 'user'
955
+ # User-specific rate limiting
956
+ rate_limit!(key: "user:#{current_user.id}", limit: 100, window: 1.hour)
957
+
958
+ when 'ip'
959
+ # IP-based rate limiting
960
+ rate_limit!(key: "ip:#{controller.request.remote_ip}", limit: 50, window: 15.minutes)
961
+
962
+ when 'api_key'
963
+ # API key-based rate limiting
964
+ api_key = action_params[:api_key]
965
+ rate_limit!(key: "api:#{api_key}", limit: 1000, window: 1.hour)
966
+
967
+ when 'global'
968
+ # Global rate limiting for expensive operations
969
+ rate_limit!(key: "global:expensive_operation", limit: 10, window: 1.minute)
970
+ end
971
+
972
+ @result = { message: "Rate limit check passed" }
973
+ end
974
+
975
+ def response
976
+ render json: @result
448
977
  end
449
978
  end
450
979
  ```
451
980
 
452
- #### Use Background Jobs for Heavy Operations
981
+ ### πŸ’° Cost-Based Rate Limiting
982
+
983
+ Assign different costs to different operations:
984
+
453
985
  ```ruby
454
- # βœ… Better approach for time-consuming operations
455
- class InitiateReportAction < ReactiveActions::ReactiveAction
986
+ class CostBasedRateLimitAction < ReactiveActions::ReactiveAction
987
+ include ReactiveActions::Concerns::RateLimiter
988
+
456
989
  def action
457
- ReportGenerationJob.perform_later(action_params)
458
- @result = { status: 'queued', job_id: SecureRandom.uuid }
990
+ operation_type = action_params[:operation]
991
+ user_key = "user:#{current_user.id}"
992
+
993
+ case operation_type
994
+ when 'search'
995
+ # Light operation: cost 1
996
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 1)
997
+
998
+ when 'export'
999
+ # Medium operation: cost 5
1000
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 5)
1001
+
1002
+ when 'bulk_import'
1003
+ # Heavy operation: cost 20
1004
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 20)
1005
+
1006
+ when 'report_generation'
1007
+ # Very heavy operation: cost 50
1008
+ rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 50)
1009
+ end
1010
+
1011
+ perform_operation(operation_type)
1012
+ end
1013
+
1014
+ def response
1015
+ render json: @result
1016
+ end
1017
+
1018
+ private
1019
+
1020
+ def perform_operation(type)
1021
+ @result = { operation: type, status: 'completed' }
459
1022
  end
460
1023
  end
461
1024
  ```
462
1025
 
463
- #### Optimize Database Queries
1026
+ ### πŸ“Š Rate Limiting Status and Management
1027
+
1028
+ Check and manage rate limiting status:
1029
+
464
1030
  ```ruby
465
- class OptimizedAction < ReactiveActions::ReactiveAction
1031
+ class RateLimitManagementAction < ReactiveActions::ReactiveAction
1032
+ include ReactiveActions::Concerns::RateLimiter
1033
+
466
1034
  def action
467
- # Use includes to avoid N+1 queries
468
- @users = User.includes(:profile, :posts)
469
- .where(id: action_params[:user_ids])
1035
+ user_key = "user:#{current_user.id}"
470
1036
 
471
- # Use select to limit returned columns
472
- @summary = User.select(:id, :name, :created_at)
473
- .where(active: true)
1037
+ case action_params[:action_type]
1038
+ when 'status'
1039
+ # Check current rate limit status without consuming a request
1040
+ status = rate_limit_status(key: user_key, limit: 100, window: 1.hour)
1041
+ @result = { rate_limit_status: status }
1042
+
1043
+ when 'check_would_exceed'
1044
+ # Check if a specific cost would exceed the limit
1045
+ cost = action_params[:cost] || 1
1046
+ would_exceed = rate_limit_would_exceed?(
1047
+ key: user_key,
1048
+ limit: 100,
1049
+ window: 1.hour,
1050
+ cost: cost
1051
+ )
1052
+ @result = { would_exceed: would_exceed, cost: cost }
1053
+
1054
+ when 'reset'
1055
+ # Reset rate limit for the user (admin functionality)
1056
+ reset_rate_limit!(key: user_key, window: 1.hour)
1057
+ @result = { message: "Rate limit reset for user", user_id: current_user.id }
1058
+
1059
+ when 'remaining'
1060
+ # Get remaining requests
1061
+ remaining = rate_limit_remaining(key: user_key, limit: 100, window: 1.hour)
1062
+ @result = { remaining: remaining }
1063
+ end
1064
+ end
1065
+
1066
+ def response
1067
+ render json: @result
474
1068
  end
475
1069
  end
476
1070
  ```
477
1071
 
478
- ### πŸ“ˆ **Monitoring and Optimization**
1072
+ ### 🌐 Global Controller-Level Rate Limiting
479
1073
 
480
- - **Monitor response times**: Actions should typically complete in < 100ms
481
- - **Use Rails logging**: ReactiveActions logs execution details at debug level
482
- - **Profile memory usage**: Large parameter sets can impact memory
483
- - **Consider caching**: Use Rails caching for frequently accessed data
1074
+ Enable global rate limiting across all ReactiveActions requests:
484
1075
 
485
- ## βš™οΈ Configuration
1076
+ ```ruby
1077
+ # config/initializers/reactive_actions.rb
1078
+ ReactiveActions.configure do |config|
1079
+ config.rate_limiting_enabled = true
1080
+ config.global_rate_limiting_enabled = true
1081
+ config.global_rate_limit = 600 # 10 requests per second
1082
+ config.global_rate_limit_window = 1.minute # per minute window
1083
+ end
1084
+ ```
1085
+
1086
+ This automatically adds rate limiting to all ReactiveActions controller requests with appropriate headers:
1087
+
1088
+ ```
1089
+ X-RateLimit-Limit: 600
1090
+ X-RateLimit-Remaining: 599
1091
+ X-RateLimit-Window: 60
1092
+ X-RateLimit-Reset: 1672531260
1093
+ Retry-After: 30 # (when rate limited)
1094
+ ```
486
1095
 
487
- The gem can be configured using an initializer (automatically created by the install generator):
1096
+ ### πŸŽ›οΈ Advanced Rate Limiting Features
488
1097
 
1098
+ #### Scoped Keys
1099
+ ```ruby
1100
+ class ScopedRateLimitAction < ReactiveActions::ReactiveAction
1101
+ include ReactiveActions::Concerns::RateLimiter
1102
+
1103
+ def action
1104
+ # Create scoped keys for different features
1105
+ api_key = rate_limit_key_for('api', identifier: current_user.id)
1106
+ search_key = rate_limit_key_for('search', identifier: current_user.id)
1107
+ upload_key = rate_limit_key_for('upload', identifier: current_user.id)
1108
+
1109
+ case action_params[:feature]
1110
+ when 'api'
1111
+ rate_limit!(key: api_key, limit: 1000, window: 1.hour)
1112
+ when 'search'
1113
+ rate_limit!(key: search_key, limit: 100, window: 1.minute)
1114
+ when 'upload'
1115
+ rate_limit!(key: upload_key, limit: 10, window: 1.minute)
1116
+ end
1117
+
1118
+ @result = { feature: action_params[:feature], status: 'allowed' }
1119
+ end
1120
+
1121
+ def response
1122
+ render json: @result
1123
+ end
1124
+ end
1125
+ ```
1126
+
1127
+ #### Custom Key Generators
489
1128
  ```ruby
490
1129
  # config/initializers/reactive_actions.rb
491
1130
  ReactiveActions.configure do |config|
492
- # Configure methods to delegate from the controller to action classes
493
- config.delegated_controller_methods += [:custom_method]
1131
+ config.rate_limiting_enabled = true
1132
+
1133
+ # Custom key generator for sophisticated rate limiting
1134
+ config.rate_limit_key_generator = ->(request, action_name) do
1135
+ # Multi-factor key generation
1136
+ user_id = request.headers['X-User-ID']
1137
+ api_key = request.headers['X-API-Key']
1138
+ user_tier = request.headers['X-User-Tier'] || 'basic'
1139
+
1140
+ if api_key.present?
1141
+ # API requests get higher limits
1142
+ "api:#{api_key}:#{action_name}"
1143
+ elsif user_id.present?
1144
+ # User-based with tier consideration
1145
+ "user:#{user_tier}:#{user_id}:#{action_name}"
1146
+ else
1147
+ # Anonymous requests get IP-based limiting
1148
+ "ip:#{request.remote_ip}:#{action_name}"
1149
+ end
1150
+ end
1151
+ end
1152
+ ```
1153
+
1154
+ #### Rate Limiting with Security Integration
1155
+ ```ruby
1156
+ class SecureRateLimitedAction < ReactiveActions::ReactiveAction
1157
+ include ReactiveActions::Concerns::RateLimiter
1158
+
1159
+ # Security checks run before rate limiting
1160
+ security_check :require_authentication
1161
+
1162
+ def action
1163
+ # Apply different limits based on user role
1164
+ limit = determine_user_limit
1165
+ window = determine_user_window
1166
+
1167
+ rate_limit!(
1168
+ key: "role:#{current_user.role}:#{current_user.id}",
1169
+ limit: limit,
1170
+ window: window
1171
+ )
1172
+
1173
+ perform_secure_operation
1174
+ end
1175
+
1176
+ def response
1177
+ render json: @result
1178
+ end
1179
+
1180
+ private
494
1181
 
495
- # Configure instance variables to delegate from the controller to action classes
496
- config.delegated_instance_variables += [:custom_variable]
1182
+ def require_authentication
1183
+ raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
1184
+ end
1185
+
1186
+ def determine_user_limit
1187
+ case current_user.role
1188
+ when 'admin'
1189
+ 1000 # Admins get higher limits
1190
+ when 'premium'
1191
+ 500 # Premium users get medium limits
1192
+ when 'basic'
1193
+ 100 # Basic users get standard limits
1194
+ else
1195
+ 50 # Default for other roles
1196
+ end
1197
+ end
1198
+
1199
+ def determine_user_window
1200
+ current_user.role == 'admin' ? 1.minute : 5.minutes
1201
+ end
1202
+
1203
+ def perform_secure_operation
1204
+ @result = {
1205
+ message: "Secure operation completed",
1206
+ user_role: current_user.role,
1207
+ rate_limit_applied: true
1208
+ }
1209
+ end
497
1210
  end
1211
+ ```
1212
+
1213
+ ### πŸ—οΈ Custom Controller Rate Limiting
498
1214
 
499
- # Set the logger for ReactiveActions
500
- ReactiveActions.logger = Rails.logger
1215
+ Add rate limiting to your own controllers:
1216
+
1217
+ ```ruby
1218
+ class ApiController < ApplicationController
1219
+ include ReactiveActions::Controller::RateLimiter
1220
+
1221
+ # Rate limit specific actions
1222
+ rate_limit_action :show, limit: 100, window: 1.minute
1223
+ rate_limit_action :create, limit: 10, window: 1.minute, only: [:create]
1224
+
1225
+ # Skip rate limiting for certain actions
1226
+ skip_rate_limiting :health_check, :status
1227
+
1228
+ def show
1229
+ # This action is automatically rate limited
1230
+ render json: { data: "API response" }
1231
+ end
1232
+
1233
+ def create
1234
+ # This action has stricter rate limiting
1235
+ render json: { created: true }
1236
+ end
1237
+
1238
+ def health_check
1239
+ # This action skips rate limiting
1240
+ render json: { status: "ok" }
1241
+ end
1242
+ end
501
1243
  ```
502
1244
 
503
- ### Advanced Configuration
1245
+ ### πŸ“ˆ Rate Limiting Monitoring and Logging
504
1246
 
505
- During installation, you can choose to configure advanced options:
1247
+ Monitor rate limiting events:
506
1248
 
507
- - **Custom Controller Methods**: Add additional controller methods to delegate to action classes
508
- - **Logging Level**: Set a custom logging level for ReactiveActions
509
- - **Instance Variables**: Configure which instance variables to delegate from controllers to actions
1249
+ ```ruby
1250
+ class MonitoredRateLimitAction < ReactiveActions::ReactiveAction
1251
+ include ReactiveActions::Concerns::RateLimiter
510
1252
 
511
- ### Default Delegated Methods
1253
+ def action
1254
+ user_key = "user:#{current_user.id}"
1255
+
1256
+ begin
1257
+ # Log rate limiting attempt
1258
+ log_rate_limit_event('attempt', {
1259
+ user_id: current_user.id,
1260
+ action: 'api_call'
1261
+ })
1262
+
1263
+ rate_limit!(key: user_key, limit: 100, window: 1.hour)
1264
+
1265
+ # Log successful rate limit check
1266
+ log_rate_limit_event('success', {
1267
+ user_id: current_user.id,
1268
+ remaining: rate_limit_remaining(key: user_key, limit: 100, window: 1.hour)
1269
+ })
1270
+
1271
+ @result = { status: 'success' }
1272
+
1273
+ rescue ReactiveActions::RateLimitExceededError => e
1274
+ # Log rate limit exceeded
1275
+ log_rate_limit_event('exceeded', {
1276
+ user_id: current_user.id,
1277
+ limit: e.limit,
1278
+ current: e.current,
1279
+ retry_after: e.retry_after
1280
+ })
1281
+
1282
+ raise e
1283
+ end
1284
+ end
512
1285
 
513
- By default, the following controller methods are available in your actions:
514
- - `render`
515
- - `redirect_to`
516
- - `head`
517
- - `params`
518
- - `session`
519
- - `cookies`
520
- - `flash`
521
- - `request`
522
- - `response`
1286
+ def response
1287
+ render json: @result
1288
+ end
1289
+ end
1290
+ ```
523
1291
 
524
- ## ❌ Error Handling
1292
+ ### πŸŽ›οΈ Rate Limiting Configuration Options
525
1293
 
526
- ReactiveActions provides structured error handling with specific error types:
1294
+ #### Enable Rate Limiting During Installation
527
1295
 
528
- - `ActionNotFoundError`: When the requested action doesn't exist
529
- - `MissingParameterError`: When required parameters are missing
530
- - `InvalidParametersError`: When parameters have invalid types or formats
531
- - `UnauthorizedError`: When the user lacks permission for the action
532
- - `ActionExecutionError`: When an error occurs during action execution
1296
+ ```bash
1297
+ # Enable rate limiting during installation
1298
+ $ rails generate reactive_actions:install --enable-rate-limiting --enable-global-rate-limiting --global-rate-limit=1000
1299
+ ```
1300
+
1301
+ #### Runtime Configuration Checks
1302
+
1303
+ ```ruby
1304
+ class ConditionalRateLimitAction < ReactiveActions::ReactiveAction
1305
+ include ReactiveActions::Concerns::RateLimiter
1306
+
1307
+ def action
1308
+ # Check if rate limiting is enabled before applying
1309
+ if rate_limiting_enabled?
1310
+ rate_limit!(key: "feature:#{action_params[:feature]}", limit: 50, window: 1.minute)
1311
+ @result = { rate_limiting: 'enabled', status: 'limited' }
1312
+ else
1313
+ @result = { rate_limiting: 'disabled', status: 'unlimited' }
1314
+ end
1315
+ end
1316
+
1317
+ def response
1318
+ render json: @result
1319
+ end
1320
+ end
1321
+ ```
533
1322
 
534
- All errors return JSON responses with consistent structure:
1323
+ ### ⚑ Rate Limiting Error Handling
1324
+
1325
+ Rate limiting errors are automatically handled and return structured responses:
535
1326
 
536
1327
  ```json
537
1328
  {
538
1329
  "success": false,
539
1330
  "error": {
540
- "type": "ActionNotFoundError",
541
- "message": "Action 'non_existent' not found",
542
- "code": "NOT_FOUND"
1331
+ "type": "RateLimitExceededError",
1332
+ "message": "Rate limit exceeded: 101/100 requests in 1 minute",
1333
+ "code": "RATE_LIMIT_EXCEEDED",
1334
+ "limit": 100,
1335
+ "window": 60,
1336
+ "retry_after": 45
543
1337
  }
544
1338
  }
545
1339
  ```
546
1340
 
547
- ## πŸ”§ Troubleshooting
1341
+ ### πŸš€ Performance Considerations
548
1342
 
549
- ### Common Issues and Solutions
1343
+ Rate limiting uses Rails cache for storage:
550
1344
 
551
- #### ❓ **Action Not Found Errors**
1345
+ - **Production**: Use Redis or Memcached for distributed caching
1346
+ - **Development**: Uses memory store automatically
1347
+ - **Test**: Uses memory store to avoid cache pollution
552
1348
 
553
- **Problem**: Getting `ActionNotFoundError` for existing actions
1349
+ ```ruby
1350
+ # config/environments/production.rb
1351
+ config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
1352
+ ```
554
1353
 
555
- **Solutions**:
556
- ```bash
557
- # 1. Check file naming convention
558
- # File: app/reactive_actions/my_action.rb
559
- # Class: MyAction
560
- # Call with: action_name: "my"
1354
+ ### πŸ”§ Rate Limiting Best Practices
561
1355
 
562
- # 2. Restart Rails to reload autoloading
563
- rails restart
1356
+ 1. **Start Conservative**: Begin with generous limits and tighten based on usage patterns
1357
+ 2. **Use Appropriate Windows**: Shorter windows (1-5 minutes) for responsive limiting
1358
+ 3. **Different Limits for Different Operations**: Heavier operations should cost more
1359
+ 4. **Monitor and Alert**: Set up monitoring for rate limit violations
1360
+ 5. **Graceful Degradation**: Provide meaningful error messages and retry guidance
1361
+ 6. **User Tier Consideration**: Different limits for different user tiers
1362
+ 7. **API Documentation**: Document rate limits in your API documentation
564
1363
 
565
- # 3. Check for syntax errors in action file
566
- rails console
567
- > MyAction # Should load without errors
568
- ```
1364
+ ## πŸ’» Simple DOM Binding Examples
569
1365
 
570
- #### ❓ **JavaScript Client Not Working**
1366
+ ### Basic Button Actions
571
1367
 
572
- **Problem**: `ReactiveActions is not defined` in browser
1368
+ ```html
1369
+ <!-- Simple button click -->
1370
+ <button reactive-action="click->test">Test Action</button>
573
1371
 
574
- **Solutions**:
575
- ```javascript
576
- // 1. Check importmap.rb includes the pin
577
- // config/importmap.rb should have:
578
- pin "reactive_actions", to: "reactive_actions.js"
579
-
580
- // 2. Check application.js imports it
581
- // app/javascript/application.js should have:
582
- import "reactive_actions"
1372
+ <!-- Button with data attributes -->
1373
+ <button reactive-action="click->update_status"
1374
+ reactive-action-status="active">
1375
+ Update Status
1376
+ </button>
583
1377
 
584
- // 3. Clear browser cache and restart Rails
1378
+ <!-- Button with HTTP method -->
1379
+ <button reactive-action="click->delete#remove_item">Delete Item</button>
585
1380
  ```
586
1381
 
587
- #### ❓ **CSRF Token Errors**
1382
+ ### Form Examples
1383
+
1384
+ ```html
1385
+ <!-- Simple form submission -->
1386
+ <form reactive-action="submit->create_item">
1387
+ <input name="title" type="text" required>
1388
+ <button type="submit">Create</button>
1389
+ </form>
1390
+
1391
+ <!-- Form with custom data -->
1392
+ <form reactive-action="submit->post#save_data"
1393
+ reactive-action-category="important">
1394
+ <input name="message" type="text" required>
1395
+ <button type="submit">Save</button>
1396
+ </form>
1397
+ ```
588
1398
 
589
- **Problem**: Getting `Can't verify CSRF token authenticity`
1399
+ ### Input Events
590
1400
 
591
- **Solutions**:
592
- ```erb
593
- <!-- 1. Ensure CSRF meta tags are in your layout -->
594
- <%= csrf_meta_tags %>
1401
+ ```html
1402
+ <!-- Live search -->
1403
+ <input type="text"
1404
+ reactive-action="input->search"
1405
+ placeholder="Search...">
595
1406
 
596
- <!-- 2. Check that protect_from_forgery is enabled -->
597
- <%# In your ApplicationController %>
598
- protect_from_forgery with: :exception
1407
+ <!-- Select dropdown -->
1408
+ <select reactive-action="change->filter_results">
1409
+ <option value="all">All Items</option>
1410
+ <option value="active">Active Only</option>
1411
+ </select>
599
1412
  ```
600
1413
 
601
- #### ❓ **Parameter Sanitization Issues**
1414
+ ### Success/Error Handling
602
1415
 
603
- **Problem**: Parameters are being rejected or modified unexpectedly
1416
+ ```html
1417
+ <button reactive-action="click->test"
1418
+ reactive-action-success="showSuccess"
1419
+ reactive-action-error="showError">
1420
+ Test with Callbacks
1421
+ </button>
604
1422
 
605
- **Solutions**:
606
- ```ruby
607
- # 1. Check parameter key format (alphanumeric, underscore, hyphen only)
608
- # βœ… Good: { user_name: "John", user-id: 123 }
609
- # ❌ Bad: { "__eval": "code", "system()": "bad" }
1423
+ <script>
1424
+ function showSuccess(response) {
1425
+ alert('Success: ' + response.message);
1426
+ }
610
1427
 
611
- # 2. Check string length limits (max 10,000 characters)
612
- # 3. Review logs for specific sanitization messages
1428
+ function showError(error) {
1429
+ alert('Error: ' + error.message);
1430
+ }
1431
+ </script>
613
1432
  ```
614
1433
 
615
- #### ❓ **Performance Issues**
1434
+ ## Security
1435
+
1436
+ ReactiveActions implements several security measures:
1437
+
1438
+ ### πŸ”’ Built-in Security Features
616
1439
 
617
- **Problem**: Actions are slow or timing out
1440
+ - **Parameter sanitization** - Input validation and safe patterns
1441
+ - **CSRF protection** - Automatic Rails CSRF token handling
1442
+ - **Code injection prevention** - Sanitized class names and parameters
1443
+ - **Length limits** - Prevents memory exhaustion attacks
1444
+
1445
+ ### πŸ›‘οΈ Security Best Practices
618
1446
 
619
- **Solutions**:
620
1447
  ```ruby
621
- # 1. Move heavy operations to background jobs
622
- class SlowAction < ReactiveActions::ReactiveAction
1448
+ # Always validate user permissions
1449
+ class SecureAction < ReactiveActions::ReactiveAction
1450
+ security_check :require_authentication
1451
+ security_check :validate_ownership
1452
+
623
1453
  def action
624
- # Instead of this:
625
- # heavy_operation
1454
+ # Validate and sanitize inputs
1455
+ user_id = action_params[:user_id].to_i
1456
+ raise ReactiveActions::InvalidParametersError if user_id <= 0
626
1457
 
627
- # Do this:
628
- HeavyOperationJob.perform_later(action_params)
629
- @result = { status: 'queued' }
1458
+ # Use strong parameters
1459
+ permitted_params = action_params.slice(:name, :email).permit!
1460
+
1461
+ @result = User.find(user_id).update(permitted_params)
630
1462
  end
631
- end
632
1463
 
633
- # 2. Optimize database queries
634
- # 3. Add caching where appropriate
635
- # 4. Monitor with Rails logs at debug level
636
- ```
1464
+ private
1465
+
1466
+ def require_authentication
1467
+ raise ReactiveActions::SecurityCheckError unless current_user
1468
+ end
637
1469
 
638
- ### Debug Mode
1470
+ def validate_ownership
1471
+ user_id = action_params[:user_id].to_i
1472
+ unless current_user.id == user_id || current_user.admin?
1473
+ raise ReactiveActions::SecurityCheckError, "Access denied"
1474
+ end
1475
+ end
1476
+ end
1477
+ ```
639
1478
 
640
- Enable detailed logging for troubleshooting:
1479
+ ## ❌ Error Handling
641
1480
 
642
- ```ruby
643
- # config/initializers/reactive_actions.rb
644
- ReactiveActions.logger.level = :debug
1481
+ ReactiveActions provides structured error handling:
645
1482
 
646
- # This will log:
647
- # - Action execution details
648
- # - Parameter sanitization steps
649
- # - Error stack traces
650
- # - Performance metrics
1483
+ ```json
1484
+ {
1485
+ "success": false,
1486
+ "error": {
1487
+ "type": "ActionNotFoundError",
1488
+ "message": "Action 'non_existent' not found",
1489
+ "code": "NOT_FOUND"
1490
+ }
1491
+ }
651
1492
  ```
652
1493
 
653
- ## πŸš„ Rails 8 Compatibility
1494
+ **Error Types:**
1495
+ - `ActionNotFoundError` - Action doesn't exist
1496
+ - `MissingParameterError` - Required parameters missing
1497
+ - `InvalidParametersError` - Invalid parameter format
1498
+ - `UnauthorizedError` - Permission denied
1499
+ - `ActionExecutionError` - Runtime execution error
1500
+ - `SecurityCheckError` - Security check failed
1501
+ - `RateLimitExceededError` - Rate limit exceeded
654
1502
 
655
- This gem is designed specifically for Rails 8 and takes advantage of:
1503
+ ## πŸš„ Rails 8 Compatibility
656
1504
 
657
- - βœ… **Propshaft** - Modern asset pipeline without compilation
658
- - βœ… **Importmap** - Native ES module support without bundling
659
- - βœ… **Rails 8 conventions** - Follows current Rails best practices
660
- - βœ… **Modern JavaScript** - ES6+ classes and async/await
661
- - βœ… **Backward compatibility** - Still works with Sprockets if needed
1505
+ Designed specifically for Rails 8:
1506
+ - βœ… **Propshaft** - Modern asset pipeline
1507
+ - βœ… **Importmap** - Native ES modules
1508
+ - βœ… **Rails 8 conventions** - Current best practices
1509
+ - βœ… **Modern JavaScript** - ES6+ features
1510
+ - βœ… **Backward compatibility** - Works with Sprockets
662
1511
 
663
1512
  ## πŸ—ΊοΈ Roadmap & Future Improvements
664
1513
 
665
- Planned improvements for ReactiveActions:
666
-
667
- * Security hooks - methods that run before actions for authentication and authorization checks
668
- * Rate limiting and throttling capabilities
669
- * Enhanced error handling with more granular error types
670
- * Action composition - ability to build complex workflows from smaller actions
671
- * Improved generators for common action patterns
672
- * Built-in testing utilities and helpers
673
- * Auto-generated API documentation
674
- * And much more
1514
+ Planned features:
1515
+ - Enhanced error handling
1516
+ - Action composition for complex workflows
1517
+ - Built-in testing utilities
1518
+ - Auto-generated API documentation
675
1519
 
676
1520
  ## πŸ› οΈ Development
677
1521
 
678
- After checking out the repo, run the following to install dependencies:
1522
+ After checking out the repo:
679
1523
 
680
1524
  ```bash
681
1525
  $ bundle install
682
- ```
683
-
684
- Then, run the tests:
685
-
686
- ```bash
687
- $ bundle exec rspec
688
- ```
689
-
690
- You can also run `bin/console` for an interactive prompt that will allow you to experiment.
691
-
692
- To install this gem onto your local machine, run:
693
-
694
- ```bash
695
- $ bundle exec rake install
1526
+ $ bundle exec rspec # Run tests
1527
+ $ bin/console # Interactive prompt
696
1528
  ```
697
1529
 
698
1530
  ## πŸ§ͺ Testing
699
1531
 
700
- The gem repository includes a dummy Rails application for development and testing purposes. To run the tests:
1532
+ Run the test suite:
701
1533
 
702
1534
  ```bash
703
1535
  $ bundle exec rspec
704
1536
  ```
705
1537
 
706
- **Note**: The dummy application is only available in the source repository and is not included in the distributed gem.
1538
+ **Note**: The dummy application is only available in the source repository.
707
1539
 
708
1540
  ## 🀝 Contributing
709
1541