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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +39 -0
  3. data/.qlty/.gitignore +7 -0
  4. data/.qlty/qlty.toml +82 -0
  5. data/.ruby-version +1 -1
  6. data/Gemfile +2 -1
  7. data/Gemfile.lock +148 -108
  8. data/README.md +627 -224
  9. data/lib/generators/steel_wheel/application_handler/templates/handler_template.rb +4 -0
  10. data/lib/generators/steel_wheel/form/USAGE +8 -0
  11. data/lib/generators/steel_wheel/form/form_generator.rb +16 -0
  12. data/lib/generators/steel_wheel/form/templates/form_template.rb +3 -0
  13. data/lib/generators/steel_wheel/handler/templates/handler_template.rb +5 -16
  14. data/lib/generators/steel_wheel/scaffold_controller/USAGE +14 -0
  15. data/lib/generators/steel_wheel/scaffold_controller/scaffold_controller_generator.rb +100 -0
  16. data/lib/generators/steel_wheel/scaffold_controller/templates/controller.rb +65 -0
  17. data/lib/generators/steel_wheel/scaffold_controller/templates/create.rb +23 -0
  18. data/lib/generators/steel_wheel/scaffold_controller/templates/form.tt +12 -0
  19. data/lib/generators/steel_wheel/scaffold_controller/templates/index.rb +17 -0
  20. data/lib/generators/steel_wheel/scaffold_controller/templates/model_form.rb +32 -0
  21. data/lib/generators/steel_wheel/scaffold_controller/templates/search_form.rb +31 -0
  22. data/lib/generators/steel_wheel/scaffold_controller/templates/update.rb +31 -0
  23. data/lib/steel_wheel/callbacks.rb +34 -0
  24. data/lib/steel_wheel/components.rb +54 -0
  25. data/lib/steel_wheel/filters.rb +36 -0
  26. data/lib/steel_wheel/handler.rb +67 -68
  27. data/lib/steel_wheel/params.rb +5 -1
  28. data/lib/steel_wheel/preconditions.rb +36 -0
  29. data/lib/steel_wheel/query/dependency_validator.rb +14 -0
  30. data/lib/steel_wheel/query/exists_validator.rb +15 -0
  31. data/lib/steel_wheel/query/verify_validator.rb +30 -0
  32. data/lib/steel_wheel/railtie.rb +50 -0
  33. data/lib/steel_wheel/shortcuts.rb +28 -0
  34. data/lib/steel_wheel/version.rb +1 -1
  35. data/lib/steel_wheel.rb +35 -6
  36. data/steel_wheel.gemspec +4 -4
  37. metadata +38 -30
  38. data/.github/workflows/ruby.yml +0 -43
  39. data/lib/generators/steel_wheel/command/USAGE +0 -8
  40. data/lib/generators/steel_wheel/command/command_generator.rb +0 -16
  41. data/lib/generators/steel_wheel/command/templates/command_template.rb +0 -5
  42. data/lib/generators/steel_wheel/params/USAGE +0 -8
  43. data/lib/generators/steel_wheel/params/params_generator.rb +0 -16
  44. data/lib/generators/steel_wheel/params/templates/params_template.rb +0 -5
  45. data/lib/generators/steel_wheel/query/USAGE +0 -8
  46. data/lib/generators/steel_wheel/query/query_generator.rb +0 -16
  47. data/lib/generators/steel_wheel/query/templates/query_template.rb +0 -5
  48. data/lib/steel_wheel/command.rb +0 -24
  49. data/lib/steel_wheel/query.rb +0 -20
  50. data/lib/steel_wheel/response.rb +0 -34
data/README.md CHANGED
@@ -3,125 +3,237 @@
3
3
  [![Test Coverage](https://api.codeclimate.com/v1/badges/a197758aa1cfde54f0e1/test_coverage)](https://codeclimate.com/github/andriy-baran/steel_wheel/test_coverage)
4
4
  [![Gem Version](https://badge.fury.io/rb/steel_wheel.svg)](https://badge.fury.io/rb/steel_wheel)
5
5
 
6
- The library is a tool for building highly structured service objects.
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
- ### Stages
11
- We may consider any controller action as a sequence of following stages:
12
- 1. **Input validations and preparations**
13
- * Describe the structure of parameters
14
- * Validate values, provide defaults
15
- 2. **Querying data and preparing context**
16
- * Records lookups by IDs in parameters
17
- * Validate permissions to perform an action
18
- * Validate conditions (business logic requirements)
19
- * Inject Dependencies
20
- * Set up current user
21
- 3. **Performing Action (skipped on GET requests)**
22
- * Updade database state
23
- * Enqueue jobs
24
- * Handle exceptions
25
- * Validate intermediate states
26
- 4. **Exposing Results/Errors**
27
- * Presenters
28
- * Contextual information useful for the users
29
-
30
- ### Implementation of stages
31
- As you can see each step has specific tasks and can be implemented as a separate object.
32
-
33
- **SteelWheel::Params (gem https://github.com/andriy-baran/easy_params)**
34
- * provides DSL for `params` structure definition
35
- * provides type coercion and default values for individual attributes
36
- * has ActionModel::Validation included
37
- * implements `http_status` method that returs HTTP error code
38
-
39
- **SteelWheel::Query**
40
- * has `Memery` module included
41
- * has ActionModel::Validation included
42
- * implements `http_status` method that returs HTTP error code
43
-
44
- **SteelWheel::Command**
45
- * has ActionModel::Validation included
46
- * implements `http_status` method that returs HTTP error code
47
- * implements `call` method that should do the stuff
48
-
49
- **SteelWheel::Response**
50
- * has ActionModel::Validation included
51
- * implements `status` method that returs HTTP error code
52
- * implements `success?` method that checks if there are any errors
53
-
54
- ### Process
55
- Let's image the process that connects stages described above
56
- * Get an input and initialize object for params, trigger callbacks
57
- * Initialize object for preparing context and give it an access to previous object, trigger callbacks
58
- * Initialize object for performing action and give it an access to previous object, trigger callbacks
59
- * Initialize resulting object and give it an access to previous object,
60
- * Run validations, collect errros, trigger callbacks
61
- * If everything is ok run action and handle errors that appear during execution time.
62
- * If we have an error on any stage we stop validating following objects.
63
-
64
- ### Callbacks
65
-
66
- We have two types of callbacks explicit and implicit
67
-
68
- ### Implicit callbacks
69
-
70
- We define them via handler instance methods
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
- def on_params_created(params)
74
- # NOOP
75
- end
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
- def on_query_created(query)
78
- # NOOP
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
- def on_command_created(command)
82
- # NOOP
83
- end
134
+ ### Advanced Filtering System
84
135
 
85
- def on_response_created(command)
86
- # NOOP
87
- end
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
- # After validation callbacks
150
+ form Products::SearchForm
90
151
 
91
- def on_failure(flow)
92
- # NOOP
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
- def on_success(flow)
96
- # NOOP
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
- ### Explicit callbacks
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
- We define them during instantiation of hanler by providing a block parameter
181
+ SteelWheel automatically handles parameter scoping and separation:
103
182
 
183
+ #### Form Scope Configuration
104
184
  ```ruby
105
- handler = handler_class.new do |c|
106
- c.params { |o| puts o }
107
- c.query { |o| puts o }
108
- c.command { |o| puts o }
109
- c.response { |o| puts o }
110
- end
111
- result = handler.handle(input: { id: 1 })
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
- In addition we can manipulate with objects directly via callback of `handle` mathod
195
+
196
+ #### Parameter Separation
114
197
  ```ruby
115
- result = handler_class.handle(input: { id: 1 }) do |c|
116
- c.params.id = 12
117
- c.query.user = current_user
118
- c.command.request_headers = request.headers
119
- c.response.prepare_presenter
120
- end
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
- $ bundle
245
+ ```bash
246
+ bundle install
247
+ ```
134
248
 
135
249
  Or install it yourself as:
136
250
 
137
- $ gem install steel_wheel
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
- Add base handler
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
- Add specific handler
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
- This will generate `app/handlers/products/create_handler.rb`. And we can customize it
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
- define do
157
- params do
158
- attribute :title, string
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
- query do
167
- validate :product, :variant
321
+ form Products::ModelForm
168
322
 
169
- memoize def new_product
170
- Product.new(title: title)
171
- end
323
+ depends_on :store, :product
324
+ finder :store, -> { Store.find(url_params.store_id) }, validate_existence: true
172
325
 
173
- memoize def new_variant
174
- new_product.build_variant(weight: weight, price: price)
175
- end
326
+ def call
327
+ @product = store.products.create!(form_params.to_h)
328
+ end
176
329
 
177
- private
330
+ def form_attributes
331
+ { model: product }
332
+ end
333
+ end
334
+ ```
178
335
 
179
- def product
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
- def variant
184
- errors.add(:base, :unprocessable_entity, new_variant.errors.full_messages.join("\n")) if new_variant.invalid?
185
- end
186
- end
338
+ #### Filtering
339
+ ```ruby
340
+ class Products::IndexHandler < ApplicationHandler
341
+ form Products::SearchForm
187
342
 
188
- command do
189
- def add_to_stock!
190
- PointOfSale.find_each do |pos|
191
- PosProductStock.create!(pos_id: pos.id, product_id: new_product.id, on_hand: 0.0)
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
- def on_success(flow)
209
- flow.call
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
- Looks too long. Lets move code into separate files.
214
- ```bash
215
- bin/rails g steel_wheel:params products/create
216
- ```
217
- Add relative code
362
+
363
+ #### Callbacks
218
364
  ```ruby
219
- # Base class also can be refered via
220
- # ApplicationHandler.main_builder.abstract_factory.params_factory.base_class
221
- class Products::CreateHandler
222
- class Params < SteelWheel::Params
223
- attribute :title, string
224
- attribute :weight, string
225
- attribute :price, string
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
- validates :title, :weight, :price, presence: true
228
- validates :weight, allow_blank: true, format: { with: /\A[0-9]+\s[g|kg]\z/ }
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
- Than do the same for query
233
- ```bash
234
- bin/rails g steel_wheel:query products/create
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
- Add code...
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
- # Base class also can be refered via
239
- # ApplicationHandler.main_builder.abstract_factory.query_factory.base_class
240
- class Products::CreateHandler
241
- class Query < SteelWheel::Query
242
- validate :product, :variant
243
-
244
- memoize def new_product
245
- Product.new(title: title)
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
- memoize def new_variant
249
- new_product.build_variant(weight: weight, price: price)
250
- end
421
+ private
251
422
 
252
- private
423
+ def create_product
424
+ @product = Product.create!(form_params.to_h)
425
+ end
253
426
 
254
- def product
255
- errors.add(:unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid?
256
- end
427
+ def update_product
428
+ product.update!(form_params.to_h)
429
+ end
257
430
 
258
- def variant
259
- errors.add(:unprocessable_entity, new_variant.errors.full_messages.join("\n")) if new_variant.invalid?
260
- end
431
+ def destroy_product
432
+ product.destroy!
261
433
  end
262
434
  end
263
435
  ```
264
- And finally command
265
- ```bash
266
- bin/rails g steel_wheel:command products/create
267
- ```
268
- Move code
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 Products::CreateHandler
271
- class Command < SteelWheel::Command
272
- def add_to_stock!
273
- ::PointOfSale.find_each do |pos|
274
- ::PosProductStock.create!(pos_id: pos.id, product_id: new_product.id, on_hand: 0.0)
275
- end
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
- def call(response)
279
- ::ApplicationRecord.transaction do
280
- new_product.save!
281
- new_variant.save!
282
- add_to_stock!
283
- rescue => e
284
- response.errors.add(:unprocessable_entity, e.message)
285
- raise ActiveRecord::Rollback
286
- end
287
- end
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
- Than we can update handler
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
- # app/handlers/manage/products/create_handler.rb
294
- class Manage::Products::CreateHandler < ApplicationHandler
295
- define do
296
- params Params
297
- query Query
298
- command Command
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
- def on_success(flow)
302
- flow.call(flow)
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 status codes and errors handling
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.add(:unprocessable_entity, 'error')
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
- As you know `full_messages` will produce `['Unprocessable Entity error']` to prevent this and get only error `SteelWheel::Response` has special method that makes some error keys to behave like `:base`
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
- # To override it in your app
318
- class SomeHandler
319
- define do
320
- response do
321
- generic_validation_keys(:not_found, :forbidden, :unprocessable_entity, :bad_request, :unauthorized, :payment_required)
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
- In Rails 6.1 `ActiveModel::Error` was introdused and previous setup is not needed, second argument is used instead
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
- errors.add(:base, :unprocessable_entity, 'error')
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.