steel_wheel 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +39 -0
- data/.qlty/.gitignore +7 -0
- data/.qlty/qlty.toml +82 -0
- data/.ruby-version +1 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +148 -108
- data/README.md +627 -224
- data/lib/generators/steel_wheel/application_handler/templates/handler_template.rb +4 -0
- data/lib/generators/steel_wheel/form/USAGE +8 -0
- data/lib/generators/steel_wheel/form/form_generator.rb +16 -0
- data/lib/generators/steel_wheel/form/templates/form_template.rb +3 -0
- data/lib/generators/steel_wheel/handler/templates/handler_template.rb +5 -16
- data/lib/generators/steel_wheel/scaffold_controller/USAGE +14 -0
- data/lib/generators/steel_wheel/scaffold_controller/scaffold_controller_generator.rb +100 -0
- data/lib/generators/steel_wheel/scaffold_controller/templates/controller.rb +65 -0
- data/lib/generators/steel_wheel/scaffold_controller/templates/create.rb +23 -0
- data/lib/generators/steel_wheel/scaffold_controller/templates/form.tt +12 -0
- data/lib/generators/steel_wheel/scaffold_controller/templates/index.rb +17 -0
- data/lib/generators/steel_wheel/scaffold_controller/templates/model_form.rb +32 -0
- data/lib/generators/steel_wheel/scaffold_controller/templates/search_form.rb +31 -0
- data/lib/generators/steel_wheel/scaffold_controller/templates/update.rb +31 -0
- data/lib/steel_wheel/callbacks.rb +34 -0
- data/lib/steel_wheel/components.rb +54 -0
- data/lib/steel_wheel/filters.rb +36 -0
- data/lib/steel_wheel/handler.rb +67 -68
- data/lib/steel_wheel/params.rb +5 -1
- data/lib/steel_wheel/preconditions.rb +36 -0
- data/lib/steel_wheel/query/dependency_validator.rb +14 -0
- data/lib/steel_wheel/query/exists_validator.rb +15 -0
- data/lib/steel_wheel/query/verify_validator.rb +30 -0
- data/lib/steel_wheel/railtie.rb +50 -0
- data/lib/steel_wheel/shortcuts.rb +28 -0
- data/lib/steel_wheel/version.rb +1 -1
- data/lib/steel_wheel.rb +35 -6
- data/steel_wheel.gemspec +4 -4
- metadata +38 -30
- data/.github/workflows/ruby.yml +0 -43
- data/lib/generators/steel_wheel/command/USAGE +0 -8
- data/lib/generators/steel_wheel/command/command_generator.rb +0 -16
- data/lib/generators/steel_wheel/command/templates/command_template.rb +0 -5
- data/lib/generators/steel_wheel/params/USAGE +0 -8
- data/lib/generators/steel_wheel/params/params_generator.rb +0 -16
- data/lib/generators/steel_wheel/params/templates/params_template.rb +0 -5
- data/lib/generators/steel_wheel/query/USAGE +0 -8
- data/lib/generators/steel_wheel/query/query_generator.rb +0 -16
- data/lib/generators/steel_wheel/query/templates/query_template.rb +0 -5
- data/lib/steel_wheel/command.rb +0 -24
- data/lib/steel_wheel/query.rb +0 -20
- data/lib/steel_wheel/response.rb +0 -34
data/README.md
CHANGED
|
@@ -3,125 +3,237 @@
|
|
|
3
3
|
[](https://codeclimate.com/github/andriy-baran/steel_wheel/test_coverage)
|
|
4
4
|
[](https://badge.fury.io/rb/steel_wheel)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
SteelWheel is a comprehensive Rails gem for building highly structured service objects with built-in validation, filtering, callbacks, and form handling capabilities.
|
|
7
7
|
|
|
8
8
|
## Concepts
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
10
|
+
SteelWheel provides a robust framework for creating service objects (handlers) that encapsulate business logic with the following key features:
|
|
11
|
+
|
|
12
|
+
### Core Components
|
|
13
|
+
|
|
14
|
+
- **Handlers**: Service objects that encapsulate business logic with built-in validation and error handling
|
|
15
|
+
- **Parameters**: Type-safe parameter validation using EasyParams
|
|
16
|
+
- **Forms**: Integration with [ActionForm::Rails::Base](https://github.com/andriy-baran/action_form) for form handling and validation
|
|
17
|
+
- **Filters**: Query filtering capabilities for data operations
|
|
18
|
+
- **Callbacks**: Success and failure callback mechanisms
|
|
19
|
+
- **Preconditions**: Multi-level validation (URL params, form params, handler validation)
|
|
20
|
+
- **Shortcuts**: Convenient methods for common patterns (dependencies, verification, finders)
|
|
21
|
+
|
|
22
|
+
### Key Features
|
|
23
|
+
|
|
24
|
+
- **Structured Validation**: Multi-level validation with proper HTTP status codes
|
|
25
|
+
- **Filtering System**: Built-in query filtering with custom filter definitions
|
|
26
|
+
- **Callback System**: Success and failure callbacks with status-specific handling
|
|
27
|
+
- **Form Integration**: Seamless integration with [ActionForm::Rails::Base](https://github.com/andriy-baran/action_form) for form handling
|
|
28
|
+
- **Generator Support**: Rails generators for rapid development
|
|
29
|
+
- **HTTP Status Management**: Automatic HTTP status code handling for different error types
|
|
30
|
+
|
|
31
|
+
## Complete Handling Process
|
|
32
|
+
|
|
33
|
+
### Operation Execution Flow
|
|
34
|
+
|
|
35
|
+
The complete operation flow follows this sequence:
|
|
36
|
+
|
|
37
|
+
1. **Initialization**
|
|
38
|
+
```ruby
|
|
39
|
+
handler = Products::CreateHandler.new(params)
|
|
40
|
+
# - Prepares input parameters
|
|
41
|
+
# - Separates URL params from form params
|
|
42
|
+
# - Sets up form scope if defined
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
2. **Validation Phase**
|
|
46
|
+
```ruby
|
|
47
|
+
handler.handle do |handler|
|
|
48
|
+
# 1. Validates url_params
|
|
49
|
+
# 2. Validates form_params (if present)
|
|
50
|
+
# 3. Validates handler custom validations
|
|
51
|
+
# 4. Calls dependencies, verifications, finders
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
3. **Execution Phase** (only if validation passes)
|
|
56
|
+
```ruby
|
|
57
|
+
# Calls on_validation_success hook
|
|
58
|
+
handler.on_validation_success
|
|
59
|
+
# Then calls the call method
|
|
60
|
+
handler.call
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
4. **Callback Phase**
|
|
64
|
+
```ruby
|
|
65
|
+
if handler.success?
|
|
66
|
+
handler.success_callback # Calls success block
|
|
67
|
+
else
|
|
68
|
+
handler.failure_callback # Calls failure block for current status
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Validation Flow
|
|
73
|
+
|
|
74
|
+
The handler validation process follows a strict three-level validation hierarchy:
|
|
75
|
+
|
|
76
|
+
1. **URL Parameters Validation** (`url_params`)
|
|
77
|
+
- Validates route parameters, query parameters, and controller params
|
|
78
|
+
- Uses `SteelWheel::Params` (extends EasyParams)
|
|
79
|
+
- Sets HTTP status to `:bad_request` on failure
|
|
80
|
+
- Errors are merged into the main handler errors
|
|
81
|
+
|
|
82
|
+
2. **Form Parameters Validation** (`form_params`)
|
|
83
|
+
- Validates form data when present
|
|
84
|
+
- Uses EasyParams for type-safe validation
|
|
85
|
+
- Sets HTTP status to `:unprocessable_entity` on failure
|
|
86
|
+
- Creates form object with validation errors
|
|
87
|
+
|
|
88
|
+
3. **Handler Validation** (custom validation)
|
|
89
|
+
- Validates business logic, dependencies, and custom rules
|
|
90
|
+
- Uses ActiveModel validations with custom validators
|
|
91
|
+
- Sets custom HTTP status codes based on error types
|
|
92
|
+
- Includes dependency, verification, and existence validations
|
|
93
|
+
|
|
94
|
+
#### Status Code Management
|
|
95
|
+
|
|
96
|
+
SteelWheel automatically manages HTTP status codes based on validation results:
|
|
97
|
+
|
|
98
|
+
- **URL Parameters Invalid**: `:bad_request` (400)
|
|
99
|
+
- **Form Parameters Invalid**: `:unprocessable_entity` (422)
|
|
100
|
+
- **Dependencies Missing**: `:not_found` (404)
|
|
101
|
+
- **Verification Failed**: `:unprocessable_entity` (422)
|
|
102
|
+
- **Custom Validation Errors**: Based on error type
|
|
103
|
+
- **Success**: `:ok` (200)
|
|
104
|
+
|
|
105
|
+
#### Error Handling and Form Integration
|
|
106
|
+
|
|
107
|
+
When validation fails, SteelWheel automatically:
|
|
108
|
+
|
|
109
|
+
1. **Sets appropriate HTTP status**
|
|
110
|
+
2. **Creates form object with errors** (if form validation failed)
|
|
111
|
+
3. **Merges all validation errors** into handler.errors, except form errors
|
|
112
|
+
4. **Calls appropriate failure callback**
|
|
113
|
+
5. **Provides form object to views** for error display
|
|
114
|
+
|
|
115
|
+
This ensures seamless integration between validation, form handling, and view rendering without manual error management.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
#### Form Integration in Controllers
|
|
72
119
|
```ruby
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
120
|
+
class ProductsController < ApplicationController
|
|
121
|
+
action :create do |handler|
|
|
122
|
+
handler.success do
|
|
123
|
+
redirect_to handler.product, notice: 'Product was successfully created.'
|
|
124
|
+
end
|
|
76
125
|
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
handler.failure do
|
|
127
|
+
@form = handler.form # <-- form with errors
|
|
128
|
+
render :new
|
|
129
|
+
end
|
|
130
|
+
end
|
|
79
131
|
end
|
|
132
|
+
```
|
|
80
133
|
|
|
81
|
-
|
|
82
|
-
# NOOP
|
|
83
|
-
end
|
|
134
|
+
### Advanced Filtering System
|
|
84
135
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
136
|
+
SteelWheel's filtering system provides automatic query filtering based on form parameters.
|
|
137
|
+
|
|
138
|
+
To implement filtering, you need to:
|
|
139
|
+
1. Define a search form that inherits from [`ActionForm::Base`](https://github.com/andriy-baran/action_form) (no authenticity token, does not support nested forms)
|
|
140
|
+
2. Create filters for each form input parameter
|
|
141
|
+
3. Set up an initial scope for the data
|
|
142
|
+
|
|
143
|
+
#### Filterable Methods
|
|
144
|
+
```ruby
|
|
145
|
+
class Products::IndexHandler < ApplicationHandler
|
|
146
|
+
def form_attributes
|
|
147
|
+
{ method: :get, action: helpers.products_path, params: form_params }
|
|
148
|
+
end
|
|
88
149
|
|
|
89
|
-
|
|
150
|
+
form Products::SearchForm
|
|
90
151
|
|
|
91
|
-
|
|
92
|
-
|
|
152
|
+
# Make a method filterable
|
|
153
|
+
filterable def products
|
|
154
|
+
helpers.current_user.includes(:category, :store)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Define filters for form parameters
|
|
158
|
+
filter :category_id do |scope, value|
|
|
159
|
+
scope.where(category_id: value)
|
|
160
|
+
end
|
|
93
161
|
end
|
|
162
|
+
```
|
|
94
163
|
|
|
95
|
-
|
|
96
|
-
|
|
164
|
+
#### Automatic Filter Application
|
|
165
|
+
```ruby
|
|
166
|
+
# In controller
|
|
167
|
+
action :index do |handler|
|
|
168
|
+
@products = handler.products # Automatically applies filters from form_params
|
|
169
|
+
@form = handler.form # Form object with search parameters
|
|
97
170
|
end
|
|
98
171
|
```
|
|
99
172
|
|
|
100
|
-
|
|
173
|
+
The filtering system automatically:
|
|
174
|
+
1. **Extracts search parameters** from form_params
|
|
175
|
+
2. **Applies corresponding filters** to the scope
|
|
176
|
+
3. **Handles missing filters** with clear error messages
|
|
177
|
+
4. **Maintains form state** for pagination and search persistence
|
|
178
|
+
|
|
179
|
+
### Parameter Processing and Scoping
|
|
101
180
|
|
|
102
|
-
|
|
181
|
+
SteelWheel automatically handles parameter scoping and separation:
|
|
103
182
|
|
|
183
|
+
#### Form Scope Configuration
|
|
104
184
|
```ruby
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
185
|
+
class Products::CreateHandler < ApplicationHandler
|
|
186
|
+
# Form parameters will be extracted from params[:product]
|
|
187
|
+
form Products::ModelForm
|
|
188
|
+
|
|
189
|
+
def call
|
|
190
|
+
# form_params contains only :product scoped parameters
|
|
191
|
+
# url_params contains route and query parameters
|
|
192
|
+
end
|
|
193
|
+
end
|
|
112
194
|
```
|
|
113
|
-
|
|
195
|
+
|
|
196
|
+
#### Parameter Separation
|
|
114
197
|
```ruby
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
198
|
+
# Input: { id: 1, product: { name: "Widget", price: 10.99 } }
|
|
199
|
+
handler = Products::CreateHandler.new(params)
|
|
200
|
+
|
|
201
|
+
handler.url_params.id # => 1
|
|
202
|
+
handler.form_params.name # => "Widget"
|
|
203
|
+
handler.form_params.price # => 10.99
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Memoization and Performance
|
|
207
|
+
|
|
208
|
+
SteelWheel includes automatic memoization for expensive operations:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class Products::ShowHandler < ApplicationHandler
|
|
212
|
+
# Memoized - only called once per request
|
|
213
|
+
finder :product, -> { Product.includes(:category, :store).find(url_params.id) }
|
|
214
|
+
|
|
215
|
+
# Memoized - only called once per request
|
|
216
|
+
finder :related_products, -> { product.related_products.limit(5) }
|
|
217
|
+
|
|
218
|
+
# Memoized - only called once per request
|
|
219
|
+
memoize def expensive_calculation
|
|
220
|
+
# Complex calculation that should only run once
|
|
221
|
+
end
|
|
222
|
+
end
|
|
121
223
|
```
|
|
122
224
|
|
|
123
225
|
## Installation
|
|
124
226
|
|
|
227
|
+
### Requirements
|
|
228
|
+
|
|
229
|
+
- Ruby >= 2.6
|
|
230
|
+
- Rails >= 3.2, < 8.1
|
|
231
|
+
- EasyParams ~> 0.6.3
|
|
232
|
+
- ActionForm (for form handling)
|
|
233
|
+
- Memery ~> 1
|
|
234
|
+
|
|
235
|
+
### Installation
|
|
236
|
+
|
|
125
237
|
Add this line to your application's Gemfile:
|
|
126
238
|
|
|
127
239
|
```ruby
|
|
@@ -130,204 +242,495 @@ gem 'steel_wheel'
|
|
|
130
242
|
|
|
131
243
|
And then execute:
|
|
132
244
|
|
|
133
|
-
|
|
245
|
+
```bash
|
|
246
|
+
bundle install
|
|
247
|
+
```
|
|
134
248
|
|
|
135
249
|
Or install it yourself as:
|
|
136
250
|
|
|
137
|
-
|
|
251
|
+
```bash
|
|
252
|
+
gem install steel_wheel
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Dependencies
|
|
256
|
+
|
|
257
|
+
SteelWheel depends on:
|
|
258
|
+
- **EasyParams**: For parameter validation and type safety
|
|
259
|
+
- **[ActionForm](https://github.com/andriy-baran/action_form)**: For form handling and validation (ActionForm::Rails::Base)
|
|
260
|
+
- **Memery**: For memoization of expensive operations
|
|
261
|
+
- **ActiveModel**: For validation and error handling
|
|
262
|
+
- **Railties**: For Rails integration and generators
|
|
138
263
|
|
|
139
264
|
## Usage
|
|
140
265
|
|
|
141
|
-
|
|
266
|
+
### Generators
|
|
267
|
+
|
|
268
|
+
SteelWheel provides several generators to quickly scaffold handlers and forms:
|
|
269
|
+
|
|
270
|
+
#### Application Handler
|
|
271
|
+
Generate a base application handler:
|
|
142
272
|
|
|
143
273
|
```bash
|
|
144
274
|
bin/rails g steel_wheel:application_handler
|
|
145
275
|
```
|
|
146
276
|
|
|
147
|
-
|
|
277
|
+
This creates `app/handlers/application_handler.rb` as a base class for all handlers.
|
|
278
|
+
|
|
279
|
+
#### Individual Handler
|
|
280
|
+
Generate a specific handler:
|
|
148
281
|
|
|
149
282
|
```bash
|
|
150
283
|
bin/rails g steel_wheel:handler products/create
|
|
151
284
|
```
|
|
152
|
-
|
|
285
|
+
|
|
286
|
+
This creates `app/handlers/products/create_handler.rb`.
|
|
287
|
+
|
|
288
|
+
#### Form Handler
|
|
289
|
+
Generate a form handler:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
bin/rails g steel_wheel:form products/model
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
This creates `app/handlers/products/model_form.rb`.
|
|
296
|
+
|
|
297
|
+
#### Scaffold Controller
|
|
298
|
+
Generate a complete CRUD controller with handlers:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
bin/rails g steel_wheel:scaffold_controller Product name:string price:decimal active:boolean
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This creates:
|
|
305
|
+
- `app/controllers/products_controller.rb`
|
|
306
|
+
- `app/handlers/products/index_handler.rb`
|
|
307
|
+
- `app/handlers/products/create_handler.rb`
|
|
308
|
+
- `app/handlers/products/update_handler.rb`
|
|
309
|
+
- `app/handlers/products/model_form.rb`
|
|
310
|
+
- `app/handlers/products/search_form.rb`
|
|
311
|
+
- `lib/templates/erb/scaffold/_form.html.erb.tt` (form template)
|
|
312
|
+
|
|
313
|
+
### Basic Handler Example
|
|
153
314
|
|
|
154
315
|
```ruby
|
|
155
316
|
class Products::CreateHandler < ApplicationHandler
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
attribute :weight, string
|
|
160
|
-
attribute :price, string
|
|
161
|
-
|
|
162
|
-
validates :title, :weight, :price, presence: true
|
|
163
|
-
validates :weight, allow_blank: true, format: { with: /\A[0-9]+\s[g|kg]\z/ }
|
|
164
|
-
end
|
|
317
|
+
url_params do
|
|
318
|
+
param :store_id, :integer, required: true
|
|
319
|
+
end
|
|
165
320
|
|
|
166
|
-
|
|
167
|
-
validate :product, :variant
|
|
321
|
+
form Products::ModelForm
|
|
168
322
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
end
|
|
323
|
+
depends_on :store, :product
|
|
324
|
+
finder :store, -> { Store.find(url_params.store_id) }, validate_existence: true
|
|
172
325
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
326
|
+
def call
|
|
327
|
+
@product = store.products.create!(form_params.to_h)
|
|
328
|
+
end
|
|
176
329
|
|
|
177
|
-
|
|
330
|
+
def form_attributes
|
|
331
|
+
{ model: product }
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
```
|
|
178
335
|
|
|
179
|
-
|
|
180
|
-
errors.add(:base, :unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid?
|
|
181
|
-
end
|
|
336
|
+
### Advanced Features
|
|
182
337
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
338
|
+
#### Filtering
|
|
339
|
+
```ruby
|
|
340
|
+
class Products::IndexHandler < ApplicationHandler
|
|
341
|
+
form Products::SearchForm
|
|
187
342
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def call(response)
|
|
196
|
-
::ApplicationRecord.transaction do
|
|
197
|
-
new_product.save!
|
|
198
|
-
new_variant.save!
|
|
199
|
-
add_to_stock!
|
|
200
|
-
rescue => e
|
|
201
|
-
response.errors.add(:unprocessable_entity, e.message)
|
|
202
|
-
raise ActiveRecord::Rollback
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
343
|
+
filterable :products
|
|
344
|
+
|
|
345
|
+
def products
|
|
346
|
+
Product.includes(:category)
|
|
206
347
|
end
|
|
207
348
|
|
|
208
|
-
|
|
209
|
-
|
|
349
|
+
filter :category_id do |scope, value|
|
|
350
|
+
scope.where(category_id: value)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
filter :min_price do |scope, value|
|
|
354
|
+
scope.where('price >= ?', value)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
filter :max_price do |scope, value|
|
|
358
|
+
scope.where('price <= ?', value)
|
|
210
359
|
end
|
|
211
360
|
end
|
|
212
361
|
```
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
bin/rails g steel_wheel:params products/create
|
|
216
|
-
```
|
|
217
|
-
Add relative code
|
|
362
|
+
|
|
363
|
+
#### Callbacks
|
|
218
364
|
```ruby
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
365
|
+
Products::CreateHandler.handle(params) do |handler|
|
|
366
|
+
handler.success do |handler|
|
|
367
|
+
# Send notification email
|
|
368
|
+
ProductMailer.created(handler.product).deliver_now
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
handler.failure(:bad_request, :unprocessable_entity) do |handler|
|
|
372
|
+
# Log validation errors
|
|
373
|
+
Rails.logger.error "Product creation failed: #{handler.errors.full_messages}"
|
|
374
|
+
end
|
|
226
375
|
|
|
227
|
-
|
|
228
|
-
|
|
376
|
+
handler.failure(:not_found) do |handler|
|
|
377
|
+
# render not found page
|
|
378
|
+
render file: Rails.root.join('public', '404.html').to_s, status: handler.http_status
|
|
229
379
|
end
|
|
230
380
|
end
|
|
231
381
|
```
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
382
|
+
|
|
383
|
+
#### Validation Success Hook
|
|
384
|
+
|
|
385
|
+
The `on_validation_success` method is a powerful hook that allows you to execute code after all validations pass but before the main business logic runs. This is perfect for:
|
|
386
|
+
|
|
387
|
+
- **Action-specific logic**: Different behavior based on the current action
|
|
388
|
+
- **Pre-processing**: Setting up data or performing preliminary operations
|
|
389
|
+
- **Conditional execution**: Deciding whether to proceed with the main operation
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
class Products::UpdateHandler < ApplicationHandler
|
|
393
|
+
def on_validation_success
|
|
394
|
+
# Only update if this is an update action
|
|
395
|
+
call if current_action.update?
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def call
|
|
399
|
+
product.update!(form_params.to_h)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
235
402
|
```
|
|
236
|
-
|
|
403
|
+
|
|
404
|
+
#### Action-Based Logic
|
|
405
|
+
|
|
406
|
+
Use `current_action` to implement different behavior based on the controller action. The `current_action` method returns a StringInquirer object, so you can use methods like `current_action.create?`, `current_action.update?`, etc.:
|
|
407
|
+
|
|
237
408
|
```ruby
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
409
|
+
class Products::CrudHandler < ApplicationHandler
|
|
410
|
+
def on_validation_success
|
|
411
|
+
case current_action
|
|
412
|
+
when 'create'
|
|
413
|
+
create_product
|
|
414
|
+
when 'update'
|
|
415
|
+
update_product
|
|
416
|
+
when 'destroy'
|
|
417
|
+
destroy_product
|
|
246
418
|
end
|
|
419
|
+
end
|
|
247
420
|
|
|
248
|
-
|
|
249
|
-
new_product.build_variant(weight: weight, price: price)
|
|
250
|
-
end
|
|
421
|
+
private
|
|
251
422
|
|
|
252
|
-
|
|
423
|
+
def create_product
|
|
424
|
+
@product = Product.create!(form_params.to_h)
|
|
425
|
+
end
|
|
253
426
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
427
|
+
def update_product
|
|
428
|
+
product.update!(form_params.to_h)
|
|
429
|
+
end
|
|
257
430
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
end
|
|
431
|
+
def destroy_product
|
|
432
|
+
product.destroy!
|
|
261
433
|
end
|
|
262
434
|
end
|
|
263
435
|
```
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
436
|
+
|
|
437
|
+
#### Pre-processing and Setup
|
|
438
|
+
|
|
439
|
+
Use the hook for setup operations that should only run after validation:
|
|
440
|
+
|
|
269
441
|
```ruby
|
|
270
|
-
class
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
end
|
|
442
|
+
class Orders::CreateHandler < ApplicationHandler
|
|
443
|
+
def on_validation_success
|
|
444
|
+
# Set up order context after validation passes
|
|
445
|
+
setup_order_context
|
|
446
|
+
call
|
|
447
|
+
end
|
|
277
448
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
449
|
+
private
|
|
450
|
+
|
|
451
|
+
def setup_order_context
|
|
452
|
+
@order = Order.new(form_params.to_h)
|
|
453
|
+
@order.user = current_user
|
|
454
|
+
@order.status = 'pending'
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def call
|
|
458
|
+
@order.save!
|
|
288
459
|
end
|
|
289
460
|
end
|
|
290
461
|
```
|
|
291
|
-
|
|
462
|
+
|
|
463
|
+
#### Query Validators
|
|
464
|
+
SteelWheel includes three custom validators for common patterns:
|
|
465
|
+
|
|
466
|
+
- **DependencyValidator**: Validates that required dependencies are present
|
|
467
|
+
- **VerifyValidator**: Validates that method results are valid (calls `valid?` on the result)
|
|
468
|
+
- **ExistsValidator**: Validates that objects exist (not blank)
|
|
469
|
+
|
|
470
|
+
#### Shortcuts
|
|
292
471
|
```ruby
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
472
|
+
class Products::UpdateHandler < ApplicationHandler
|
|
473
|
+
# Define dependencies that must be present
|
|
474
|
+
# errors.add :base, :not_found, message: "Product is missing"
|
|
475
|
+
depends_on :product
|
|
476
|
+
depends_on :store, validate_provided: { message: 'record is required' }
|
|
477
|
+
|
|
478
|
+
# Define verification methods
|
|
479
|
+
# same as
|
|
480
|
+
# if category.invalid?
|
|
481
|
+
# category.errors.each do |error|
|
|
482
|
+
# errors.add(:category, :unprocessable_entity, message: error.message)
|
|
483
|
+
# end
|
|
484
|
+
# end
|
|
485
|
+
verify def category
|
|
486
|
+
Category.new(url_params.category_name)
|
|
299
487
|
end
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
488
|
+
def entity= Entity.new
|
|
489
|
+
verify :entity, valid: {
|
|
490
|
+
base: true,
|
|
491
|
+
message: { base: true,
|
|
492
|
+
id: -> (object, data) { "Please provide valid entity id" }, # message per attribute
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
# Define finders with existence validation
|
|
497
|
+
# errors.add(:product, :not_found, message: 'not found')
|
|
498
|
+
finder :product, -> { owner.products.find(url_params.id) },
|
|
499
|
+
validate_existence: {
|
|
500
|
+
base: true, # add to base
|
|
501
|
+
message: -> (object, data) { "Couldn't find Product with 'id'=#{object.url_params.id}" }
|
|
502
|
+
}
|
|
503
|
+
finder :store, -> { product.store }, validate_existence: true
|
|
504
|
+
|
|
505
|
+
def owner
|
|
506
|
+
helpers.current_user
|
|
303
507
|
end
|
|
304
508
|
end
|
|
305
509
|
```
|
|
306
510
|
|
|
307
|
-
### HTTP
|
|
511
|
+
### HTTP Status Codes and Error Handling
|
|
512
|
+
|
|
513
|
+
SteelWheel provides comprehensive HTTP status code management for different types of errors:
|
|
514
|
+
|
|
515
|
+
#### Adding Errors with Status Codes
|
|
308
516
|
|
|
309
|
-
It's important to provide a correct HTTP status when we faced some problem(s) during request handling. The library encourages developers to add the status codes when they add errors.
|
|
310
517
|
```ruby
|
|
311
|
-
errors
|
|
518
|
+
# Add errors with specific HTTP status codes as errror details
|
|
519
|
+
errors.add(:base, :unprocessable_entity, 'Validation failed')
|
|
520
|
+
errors.add(:base, :not_found, 'Resource not found')
|
|
521
|
+
errors.add(:base, :forbidden, 'Access denied')
|
|
312
522
|
```
|
|
313
|
-
|
|
523
|
+
|
|
524
|
+
#### Generic Validation Keys (Rails < 6.1)
|
|
525
|
+
|
|
526
|
+
For Rails versions before 6.1, SteelWheel provides `generic_validation_keys` to prevent status codes from appearing in error messages:
|
|
527
|
+
|
|
314
528
|
```ruby
|
|
315
529
|
# Default setup
|
|
316
530
|
generic_validation_keys(:not_found, :forbidden, :unprocessable_entity, :bad_request, :unauthorized)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
531
|
+
|
|
532
|
+
# Custom setup in your handler
|
|
533
|
+
class SomeHandler < ApplicationHandler
|
|
534
|
+
generic_validation_keys(:not_found, :forbidden, :unprocessable_entity, :bad_request, :unauthorized, :payment_required)
|
|
535
|
+
end
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### Rails 6.1+ Error Handling
|
|
539
|
+
|
|
540
|
+
In Rails 6.1+, use the second argument for error types:
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
errors.add(:base, :unprocessable_entity, 'Validation failed')
|
|
544
|
+
errors.add(:base, :not_found, 'Resource not found')
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
#### Automatic Status Code Handling
|
|
548
|
+
|
|
549
|
+
SteelWheel automatically sets HTTP status codes based on validation results:
|
|
550
|
+
|
|
551
|
+
- **URL Parameters**: `:bad_request` for invalid URL parameters
|
|
552
|
+
- **Form Parameters**: `:unprocessable_entity` for invalid form data
|
|
553
|
+
- **Handler Validation**: Custom status codes based on error types
|
|
554
|
+
|
|
555
|
+
### Controller Integration
|
|
556
|
+
|
|
557
|
+
#### Basic Controller Usage
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
class ApplicationController < ActionController::Base
|
|
561
|
+
#
|
|
562
|
+
def failure_callbacks(handler)
|
|
563
|
+
handler.failure(:not_found) do
|
|
564
|
+
render file: Rails.root.join('public', '404.html').to_s, status: handler.http_status
|
|
322
565
|
end
|
|
323
566
|
end
|
|
324
567
|
end
|
|
325
568
|
```
|
|
326
|
-
|
|
569
|
+
#### Rails Integration with `action` Helper
|
|
570
|
+
|
|
571
|
+
SteelWheel provides Rails integration through a Railtie that adds a convenient `action` helper method to controllers. This helper automatically integrates handlers with controllers and sets up the `helpers` attribute for view context access:
|
|
572
|
+
|
|
327
573
|
```ruby
|
|
328
|
-
|
|
574
|
+
class ProductsController < ApplicationController
|
|
575
|
+
before_action :authenticate_account!
|
|
576
|
+
|
|
577
|
+
# GET /products
|
|
578
|
+
action :index do |handler|
|
|
579
|
+
@products = handler.products
|
|
580
|
+
@form = handler.form
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# GET /products/1
|
|
584
|
+
action :show, handler: :update do |handler|
|
|
585
|
+
@product = handler.product
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# GET /products/new
|
|
589
|
+
action :new, handler: :create do |handler|
|
|
590
|
+
@product = handler.product
|
|
591
|
+
@form = handler.form
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# GET /products/1/edit
|
|
595
|
+
action :edit, handler: :update do |handler|
|
|
596
|
+
@product = handler.product
|
|
597
|
+
@form = handler.form
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# POST /products
|
|
601
|
+
action :create do |handler|
|
|
602
|
+
handler.success do
|
|
603
|
+
redirect_to handler.product, notice: 'Product was successfully created.'
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
handler.failure do
|
|
607
|
+
@product = handler.product
|
|
608
|
+
@form = handler.form
|
|
609
|
+
render :new
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# PATCH/PUT /products/1
|
|
614
|
+
action :update do |handler|
|
|
615
|
+
handler.success do
|
|
616
|
+
redirect_to handler.product, notice: 'Product was successfully updated.'
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
handler.failure do
|
|
620
|
+
@form = handler.form
|
|
621
|
+
@product = handler.product
|
|
622
|
+
render :edit
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# DELETE /products/1
|
|
627
|
+
action :destroy, handler: :update do |handler|
|
|
628
|
+
redirect_to products_url, notice: 'Product was successfully destroyed.'
|
|
629
|
+
end
|
|
630
|
+
end
|
|
329
631
|
```
|
|
330
632
|
|
|
633
|
+
#### Generated Controller Features
|
|
634
|
+
|
|
635
|
+
The scaffold controller generator creates controllers with:
|
|
636
|
+
|
|
637
|
+
- **Automatic Handler Integration**: Uses the `action` helper for seamless handler integration
|
|
638
|
+
- **Success/Failure Callbacks**: Built-in success and failure handling with appropriate redirects
|
|
639
|
+
- **Form Integration**: Automatic form object creation and error handling using [ActionForm](https://github.com/andriy-baran/action_form)
|
|
640
|
+
- **RESTful Actions**: Complete CRUD operations (index, show, new, create, edit, update, destroy)
|
|
641
|
+
- **Flash Messages**: Success and error notifications
|
|
642
|
+
- **Error Rendering**: Automatic error form rendering for create/update actions
|
|
643
|
+
- **Form Templates**: Creates ERB form templates for scaffold forms
|
|
644
|
+
- **Helper Integration**: Sets up view context through the `helpers` attribute
|
|
645
|
+
|
|
646
|
+
#### Action Helper Parameters
|
|
647
|
+
|
|
648
|
+
The `action` helper accepts several parameters:
|
|
649
|
+
|
|
650
|
+
- `action_name` - The controller action name
|
|
651
|
+
- `handler` - Handler name (defaults to action_name)
|
|
652
|
+
- `&block` - Block executed with handler instance
|
|
653
|
+
|
|
654
|
+
#### Handler Class Resolution
|
|
655
|
+
|
|
656
|
+
SteelWheel automatically resolves handler classes based on the `handler` parameter:
|
|
657
|
+
|
|
658
|
+
- **Same namespace**: `handler: 'update'` → `Products::UpdateHandler`
|
|
659
|
+
- **Different namespace**: `handler: 'shared/update'` → `Shared::UpdateHandler`
|
|
660
|
+
- **Default behavior**: `action :destroy` → `handler: 'destroy'` → `Products::DestroyHandler`
|
|
661
|
+
|
|
662
|
+
## API Reference
|
|
663
|
+
|
|
664
|
+
### Handler Methods
|
|
665
|
+
|
|
666
|
+
#### Core Methods
|
|
667
|
+
- `call` - Main business logic method (must be implemented by subclasses)
|
|
668
|
+
- `handle(&block)` - Execute handler with optional block
|
|
669
|
+
- `on_validation_success` - Hook called after validation passes, before `call` method
|
|
670
|
+
- `success?` - Returns true if handler executed successfully
|
|
671
|
+
- `status` - Returns HTTP status code based on validation results
|
|
672
|
+
|
|
673
|
+
#### Parameter Methods
|
|
674
|
+
- `url_params` - Access validated URL parameters (SteelWheel::Params instance)
|
|
675
|
+
- `form_params` - Access validated form parameters (EasyParams instance)
|
|
676
|
+
- `form(attrs)` - Get form object with attributes ([ActionForm::Rails::Base](https://github.com/andriy-baran/action_form) instance)
|
|
677
|
+
- `form_attributes` - Must be implemented to return hash of attributes for form
|
|
678
|
+
- `current_action` - Returns the current controller action as a StringInquirer object
|
|
679
|
+
|
|
680
|
+
#### Validation Methods
|
|
681
|
+
- `depends_on(*attrs, validate_provided: true)` - Define required dependencies
|
|
682
|
+
- `verify(*attrs, valid: true)` - Define method that will call `valid?` on method and copy errors onto handler object
|
|
683
|
+
- `finder(name, scope, validate_existence: false)` - Define finder methods with optional existence validation
|
|
684
|
+
|
|
685
|
+
#### Filtering Methods
|
|
686
|
+
- `filter(name, &block)` - Define a filter for query parameters
|
|
687
|
+
- `filterable(name)` - Make a method filterable with form parameters
|
|
688
|
+
- `apply_filters(scope, search_params)` - Apply filters to a scope
|
|
689
|
+
|
|
690
|
+
#### Callback Methods
|
|
691
|
+
- `success(&block)` - Define success callback
|
|
692
|
+
- `failure(*statuses, &block)` - Define failure callback for specific status
|
|
693
|
+
|
|
694
|
+
### Class Methods
|
|
695
|
+
|
|
696
|
+
#### Generators
|
|
697
|
+
- `rails g steel_wheel:application_handler` - Generate base application handler
|
|
698
|
+
- `rails g steel_wheel:handler Name` - Generate specific handler
|
|
699
|
+
- `rails g steel_wheel:form Name` - Generate form handler
|
|
700
|
+
- `rails g steel_wheel:scaffold_controller Name attributes` - Generate CRUD scaffold
|
|
701
|
+
|
|
702
|
+
#### Configuration
|
|
703
|
+
- `url_params(klass = nil, &block)` - Define URL parameter validation
|
|
704
|
+
- `form(klass = nil, &block)` - Define form validation
|
|
705
|
+
- `generic_validation_keys(*keys)` - Configure generic validation keys (Rails < 6.1)
|
|
706
|
+
|
|
707
|
+
### Rails Integration Methods
|
|
708
|
+
|
|
709
|
+
#### Railtie Integration
|
|
710
|
+
SteelWheel automatically integrates with Rails through a Railtie that:
|
|
711
|
+
- Adds the `action` helper method to all controllers
|
|
712
|
+
- Sets up the `helpers` attribute for view context access
|
|
713
|
+
- Provides automatic handler class resolution
|
|
714
|
+
- Includes default failure callbacks for 404 errors
|
|
715
|
+
|
|
716
|
+
#### Controller Helpers
|
|
717
|
+
- `action(action_name, handler: action_name, &block)` - Define controller action with handler integration
|
|
718
|
+
- `handler_class_for(handler)` - Resolve handler class for controller action
|
|
719
|
+
- `failure_callbacks(handler)` - Set up default failure callbacks for handlers
|
|
720
|
+
|
|
721
|
+
#### Handler Attributes
|
|
722
|
+
- `helpers` - View context (set automatically by action helper)
|
|
723
|
+
- `http_status` - Current HTTP status code
|
|
724
|
+
- `input` - Raw input parameters from controllers
|
|
725
|
+
- `form_input` - Form-specific input parameters
|
|
726
|
+
|
|
727
|
+
### Error Classes
|
|
728
|
+
|
|
729
|
+
- `SteelWheel::Error` - Base error class
|
|
730
|
+
- `SteelWheel::ActionNotImplementedError` - Raised when `call` method is not implemented
|
|
731
|
+
- `SteelWheel::FilterNotImplementedError` - Raised when a filter is not defined
|
|
732
|
+
- `SteelWheel::FormAttributesNotImplementedError` - Raised when `form_attributes` is not implemented
|
|
733
|
+
|
|
331
734
|
## Development
|
|
332
735
|
|
|
333
736
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|