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.
- checksums.yaml +4 -4
- data/README.md +1156 -324
- data/app/assets/javascripts/reactive_actions.js +271 -13
- data/app/controllers/reactive_actions/reactive_actions_controller.rb +5 -1
- data/lib/generators/reactive_actions/install/install_generator.rb +201 -35
- data/lib/generators/reactive_actions/install/templates/README +96 -20
- data/lib/generators/reactive_actions/install/templates/initializer.rb +59 -0
- data/lib/reactive_actions/concerns/rate_limiter.rb +174 -0
- data/lib/reactive_actions/concerns/security_checks.rb +108 -0
- data/lib/reactive_actions/configuration.rb +23 -3
- data/lib/reactive_actions/controller/rate_limiter.rb +187 -0
- data/lib/reactive_actions/errors.rb +17 -0
- data/lib/reactive_actions/rate_limiter.rb +165 -0
- data/lib/reactive_actions/reactive_action.rb +5 -0
- data/lib/reactive_actions/version.rb +1 -1
- data/lib/reactive_actions.rb +5 -1
- metadata +6 -2
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.
|
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.
|
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
|
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
|
-
###
|
179
|
+
### DOM Binding (Recommended)
|
149
180
|
|
150
|
-
|
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
|
-
|
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 /
|
212
|
+
GET/POST/PUT/PATCH/DELETE /reactive_actions/execute
|
160
213
|
```
|
161
214
|
|
162
|
-
|
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
|
-
###
|
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
|
-
|
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[:
|
185
|
-
user.update(action_params[:
|
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
|
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
|
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
|
-
|
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
|
-
|
499
|
+
def response
|
500
|
+
render json: @result, status: :created
|
501
|
+
end
|
502
|
+
end
|
235
503
|
|
236
|
-
|
237
|
-
|
238
|
-
class ProcessPaymentAction < ReactiveActions::ReactiveAction
|
504
|
+
# app/reactive_actions/update_user_action.rb
|
505
|
+
class UpdateUserAction < ReactiveActions::ReactiveAction
|
239
506
|
def action
|
240
|
-
|
241
|
-
|
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
|
-
|
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
|
-
|
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
|
275
|
-
|
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
|
-
####
|
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
|
-
##
|
578
|
+
## π Security Checks
|
315
579
|
|
316
|
-
ReactiveActions
|
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
|
-
###
|
582
|
+
### Basic Security Checks
|
319
583
|
|
320
|
-
|
584
|
+
Add security checks to your actions using the `security_check` class method:
|
321
585
|
|
322
|
-
```
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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
|
-
|
334
|
-
|
335
|
-
|
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
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
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
|
-
###
|
608
|
+
### Multiple Security Checks
|
348
609
|
|
349
|
-
|
610
|
+
Chain multiple security checks for layered protection:
|
350
611
|
|
351
|
-
```
|
352
|
-
|
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
|
-
|
355
|
-
|
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
|
-
###
|
639
|
+
### Lambda-Based Security Checks
|
359
640
|
|
360
|
-
|
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
|
-
|
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
|
-
|
661
|
+
def response
|
662
|
+
render json: @result
|
663
|
+
end
|
664
|
+
end
|
665
|
+
```
|
371
666
|
|
372
|
-
###
|
667
|
+
### Conditional Security Checks
|
373
668
|
|
374
|
-
|
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
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
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
|
-
|
386
|
-
|
387
|
-
|
388
|
-
- **Parameter filtering**: Recursive parameter sanitization for nested structures
|
683
|
+
def action
|
684
|
+
@result = { message: "Conditional security checks passed" }
|
685
|
+
end
|
389
686
|
|
390
|
-
|
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/
|
394
|
-
class
|
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
|
-
|
397
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
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
|
-
|
404
|
-
|
869
|
+
unless api_key.present? && ApiKey.valid?(api_key)
|
870
|
+
raise ReactiveActions::SecurityCheckError, "Invalid or missing API key"
|
871
|
+
end
|
405
872
|
|
406
|
-
@
|
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
|
-
|
886
|
+
## π¦ Rate Limiting
|
412
887
|
|
413
|
-
|
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
|
-
|
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
|
-
###
|
912
|
+
### Configuration Options
|
422
913
|
|
423
|
-
|
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
|
-
|
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
|
-
|
924
|
+
Include the `RateLimiter` concern in your actions to add rate limiting functionality:
|
431
925
|
|
432
|
-
#### Action Design
|
433
926
|
```ruby
|
434
|
-
#
|
435
|
-
class
|
927
|
+
# app/reactive_actions/api_action.rb
|
928
|
+
class ApiAction < ReactiveActions::ReactiveAction
|
929
|
+
include ReactiveActions::Concerns::RateLimiter
|
930
|
+
|
436
931
|
def action
|
437
|
-
|
438
|
-
|
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
|
-
|
447
|
-
|
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
|
-
|
981
|
+
### π° Cost-Based Rate Limiting
|
982
|
+
|
983
|
+
Assign different costs to different operations:
|
984
|
+
|
453
985
|
```ruby
|
454
|
-
|
455
|
-
|
986
|
+
class CostBasedRateLimitAction < ReactiveActions::ReactiveAction
|
987
|
+
include ReactiveActions::Concerns::RateLimiter
|
988
|
+
|
456
989
|
def action
|
457
|
-
|
458
|
-
|
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
|
-
|
1026
|
+
### π Rate Limiting Status and Management
|
1027
|
+
|
1028
|
+
Check and manage rate limiting status:
|
1029
|
+
|
464
1030
|
```ruby
|
465
|
-
class
|
1031
|
+
class RateLimitManagementAction < ReactiveActions::ReactiveAction
|
1032
|
+
include ReactiveActions::Concerns::RateLimiter
|
1033
|
+
|
466
1034
|
def action
|
467
|
-
|
468
|
-
@users = User.includes(:profile, :posts)
|
469
|
-
.where(id: action_params[:user_ids])
|
1035
|
+
user_key = "user:#{current_user.id}"
|
470
1036
|
|
471
|
-
|
472
|
-
|
473
|
-
|
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
|
-
###
|
1072
|
+
### π Global Controller-Level Rate Limiting
|
479
1073
|
|
480
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
493
|
-
|
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
|
-
|
496
|
-
|
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
|
-
|
500
|
-
|
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
|
-
###
|
1245
|
+
### π Rate Limiting Monitoring and Logging
|
504
1246
|
|
505
|
-
|
1247
|
+
Monitor rate limiting events:
|
506
1248
|
|
507
|
-
|
508
|
-
|
509
|
-
|
1249
|
+
```ruby
|
1250
|
+
class MonitoredRateLimitAction < ReactiveActions::ReactiveAction
|
1251
|
+
include ReactiveActions::Concerns::RateLimiter
|
510
1252
|
|
511
|
-
|
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
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
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
|
-
|
1292
|
+
### ποΈ Rate Limiting Configuration Options
|
525
1293
|
|
526
|
-
|
1294
|
+
#### Enable Rate Limiting During Installation
|
527
1295
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
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
|
-
|
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": "
|
541
|
-
"message": "
|
542
|
-
"code": "
|
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
|
-
|
1341
|
+
### π Performance Considerations
|
548
1342
|
|
549
|
-
|
1343
|
+
Rate limiting uses Rails cache for storage:
|
550
1344
|
|
551
|
-
|
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
|
-
|
1349
|
+
```ruby
|
1350
|
+
# config/environments/production.rb
|
1351
|
+
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
1352
|
+
```
|
554
1353
|
|
555
|
-
|
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
|
-
|
563
|
-
|
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
|
-
|
566
|
-
rails console
|
567
|
-
> MyAction # Should load without errors
|
568
|
-
```
|
1364
|
+
## π» Simple DOM Binding Examples
|
569
1365
|
|
570
|
-
|
1366
|
+
### Basic Button Actions
|
571
1367
|
|
572
|
-
|
1368
|
+
```html
|
1369
|
+
<!-- Simple button click -->
|
1370
|
+
<button reactive-action="click->test">Test Action</button>
|
573
1371
|
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
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
|
-
|
1378
|
+
<!-- Button with HTTP method -->
|
1379
|
+
<button reactive-action="click->delete#remove_item">Delete Item</button>
|
585
1380
|
```
|
586
1381
|
|
587
|
-
|
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
|
-
|
1399
|
+
### Input Events
|
590
1400
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
1401
|
+
```html
|
1402
|
+
<!-- Live search -->
|
1403
|
+
<input type="text"
|
1404
|
+
reactive-action="input->search"
|
1405
|
+
placeholder="Search...">
|
595
1406
|
|
596
|
-
<!--
|
597
|
-
|
598
|
-
|
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
|
-
|
1414
|
+
### Success/Error Handling
|
602
1415
|
|
603
|
-
|
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
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
# β Bad: { "__eval": "code", "system()": "bad" }
|
1423
|
+
<script>
|
1424
|
+
function showSuccess(response) {
|
1425
|
+
alert('Success: ' + response.message);
|
1426
|
+
}
|
610
1427
|
|
611
|
-
|
612
|
-
|
1428
|
+
function showError(error) {
|
1429
|
+
alert('Error: ' + error.message);
|
1430
|
+
}
|
1431
|
+
</script>
|
613
1432
|
```
|
614
1433
|
|
615
|
-
|
1434
|
+
## Security
|
1435
|
+
|
1436
|
+
ReactiveActions implements several security measures:
|
1437
|
+
|
1438
|
+
### π Built-in Security Features
|
616
1439
|
|
617
|
-
**
|
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
|
-
#
|
622
|
-
class
|
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
|
-
#
|
625
|
-
|
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
|
-
#
|
628
|
-
|
629
|
-
|
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
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
1464
|
+
private
|
1465
|
+
|
1466
|
+
def require_authentication
|
1467
|
+
raise ReactiveActions::SecurityCheckError unless current_user
|
1468
|
+
end
|
637
1469
|
|
638
|
-
|
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
|
-
|
1479
|
+
## β Error Handling
|
641
1480
|
|
642
|
-
|
643
|
-
# config/initializers/reactive_actions.rb
|
644
|
-
ReactiveActions.logger.level = :debug
|
1481
|
+
ReactiveActions provides structured error handling:
|
645
1482
|
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
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
|
-
|
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
|
-
|
1503
|
+
## π Rails 8 Compatibility
|
656
1504
|
|
657
|
-
|
658
|
-
- β
**
|
659
|
-
- β
**
|
660
|
-
- β
**
|
661
|
-
- β
**
|
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
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
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
|
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
|
-
|
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
|
1538
|
+
**Note**: The dummy application is only available in the source repository.
|
707
1539
|
|
708
1540
|
## π€ Contributing
|
709
1541
|
|