steel_wheel 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +2 -0
  3. data/.github/workflows/ruby.yml +21 -26
  4. data/.gitignore +0 -0
  5. data/.rspec +0 -0
  6. data/.rubocop.yml +188 -0
  7. data/.ruby-version +1 -1
  8. data/.travis.yml +0 -0
  9. data/CODE_OF_CONDUCT.md +0 -0
  10. data/Gemfile +10 -0
  11. data/Gemfile.lock +114 -95
  12. data/LICENSE.txt +0 -0
  13. data/README.md +307 -4
  14. data/Rakefile +7 -1
  15. data/assets/action_diagram.png +0 -0
  16. data/bin/console +1 -0
  17. data/lib/generators/steel_wheel/application_handler/USAGE +8 -0
  18. data/lib/generators/steel_wheel/application_handler/application_handler_generator.rb +12 -0
  19. data/lib/generators/steel_wheel/application_handler/templates/handler_template.rb +4 -0
  20. data/lib/generators/steel_wheel/command/USAGE +0 -0
  21. data/lib/generators/steel_wheel/command/command_generator.rb +10 -10
  22. data/lib/generators/steel_wheel/command/templates/command_template.rb +3 -1
  23. data/lib/generators/steel_wheel/handler/USAGE +0 -0
  24. data/lib/generators/steel_wheel/handler/handler_generator.rb +10 -10
  25. data/lib/generators/steel_wheel/handler/templates/handler_template.rb +13 -7
  26. data/lib/generators/steel_wheel/params/USAGE +0 -0
  27. data/lib/generators/steel_wheel/params/params_generator.rb +10 -10
  28. data/lib/generators/steel_wheel/params/templates/params_template.rb +3 -1
  29. data/lib/generators/steel_wheel/query/USAGE +0 -0
  30. data/lib/generators/steel_wheel/query/query_generator.rb +10 -10
  31. data/lib/generators/steel_wheel/query/templates/query_template.rb +3 -1
  32. data/lib/steel_wheel/command.rb +8 -2
  33. data/lib/steel_wheel/handler.rb +86 -29
  34. data/lib/steel_wheel/params.rb +10 -1
  35. data/lib/steel_wheel/query.rb +7 -1
  36. data/lib/steel_wheel/response.rb +10 -5
  37. data/lib/steel_wheel/skip_active_model_errors_keys.rb +12 -1
  38. data/lib/steel_wheel/version.rb +3 -1
  39. data/lib/steel_wheel.rb +3 -1
  40. data/steel_wheel.gemspec +6 -11
  41. metadata +21 -101
  42. data/lib/generators/steel_wheel/generic_generator.rb +0 -23
data/README.md CHANGED
@@ -1,7 +1,124 @@
1
1
  # SteelWheel
2
- [![Maintainability](https://api.codeclimate.com/v1/badges/a197758aa1cfde54f0e1/maintainability)](https://codeclimate.com/github/andriy-baran/steel_wheel/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/a197758aa1cfde54f0e1/test_coverage)](https://codeclimate.com/github/andriy-baran/steel_wheel/test_coverage)
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/a197758aa1cfde54f0e1/maintainability)](https://codeclimate.com/github/andriy-baran/steel_wheel/maintainability)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/a197758aa1cfde54f0e1/test_coverage)](https://codeclimate.com/github/andriy-baran/steel_wheel/test_coverage)
4
+ [![Gem Version](https://badge.fury.io/rb/steel_wheel.svg)](https://badge.fury.io/rb/steel_wheel)
3
5
 
4
- Allows to operate on any sequence of procedures as on linked list. It means that inserting and deleting any list/element into/from similar lists is very efficient. And a rich callbacks system gives entire control over the execution. Currently there is one example implemented. It helps organize common procedures in a Rails' controlers.
6
+ The library is a tool for building highly structured service objects.
7
+
8
+ ## Concepts
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
+
72
+ ```ruby
73
+ def on_params_created(params)
74
+ # NOOP
75
+ end
76
+
77
+ def on_query_created(query)
78
+ # NOOP
79
+ end
80
+
81
+ def on_command_created(command)
82
+ # NOOP
83
+ end
84
+
85
+ def on_response_created(command)
86
+ # NOOP
87
+ end
88
+
89
+ # After validation callbacks
90
+
91
+ def on_failure(flow)
92
+ # NOOP
93
+ end
94
+
95
+ def on_success(flow)
96
+ # NOOP
97
+ end
98
+ ```
99
+
100
+ ### Explicit callbacks
101
+
102
+ We define them during instantiation of hanler by providing a block parameter
103
+
104
+ ```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 })
112
+ ```
113
+ In addition we can manipulate with objects directly via callback of `handle` mathod
114
+ ```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
121
+ ```
5
122
 
6
123
  ## Installation
7
124
 
@@ -21,9 +138,195 @@ Or install it yourself as:
21
138
 
22
139
  ## Usage
23
140
 
24
- [Getting started on Rails](https://github.com/andriy-baran/steel_wheel/wiki/Getting-started-on-Rails)
141
+ Add base handler
142
+
143
+ ```bash
144
+ bin/rails g steel_wheel:application_handler
145
+ ```
146
+
147
+ Add specific handler
148
+
149
+ ```bash
150
+ bin/rails g steel_wheel:handler products/create
151
+ ```
152
+ This will generate `app/handlers/products/create_handler.rb`. And we can customize it
153
+
154
+ ```ruby
155
+ 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
25
165
 
26
- [Wiki](https://github.com/andriy-baran/steel_wheel/wiki)
166
+ query do
167
+ validate :product, :variant
168
+
169
+ memoize def new_product
170
+ Product.new(title: title)
171
+ end
172
+
173
+ memoize def new_variant
174
+ new_product.build_variant(weight: weight, price: price)
175
+ end
176
+
177
+ private
178
+
179
+ def product
180
+ errors.add(:base, :unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid?
181
+ end
182
+
183
+ def variant
184
+ errors.add(:base, :unprocessable_entity, new_variant.errors.full_messages.join("\n")) if new_variant.invalid?
185
+ end
186
+ end
187
+
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
206
+ end
207
+
208
+ def on_success(flow)
209
+ flow.call
210
+ end
211
+ end
212
+ ```
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
218
+ ```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
226
+
227
+ validates :title, :weight, :price, presence: true
228
+ validates :weight, allow_blank: true, format: { with: /\A[0-9]+\s[g|kg]\z/ }
229
+ end
230
+ end
231
+ ```
232
+ Than do the same for query
233
+ ```bash
234
+ bin/rails g steel_wheel:query products/create
235
+ ```
236
+ Add code...
237
+ ```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)
246
+ end
247
+
248
+ memoize def new_variant
249
+ new_product.build_variant(weight: weight, price: price)
250
+ end
251
+
252
+ private
253
+
254
+ def product
255
+ errors.add(:unprocessable_entity, new_product.errors.full_messages.join("\n")) if new_product.invalid?
256
+ end
257
+
258
+ def variant
259
+ errors.add(:unprocessable_entity, new_variant.errors.full_messages.join("\n")) if new_variant.invalid?
260
+ end
261
+ end
262
+ end
263
+ ```
264
+ And finally command
265
+ ```bash
266
+ bin/rails g steel_wheel:command products/create
267
+ ```
268
+ Move code
269
+ ```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
277
+
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
288
+ end
289
+ end
290
+ ```
291
+ Than we can update handler
292
+ ```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
299
+ end
300
+
301
+ def on_success(flow)
302
+ flow.call(flow)
303
+ end
304
+ end
305
+ ```
306
+
307
+ ### HTTP status codes and errors handling
308
+
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
+ ```ruby
311
+ errors.add(:unprocessable_entity, 'error')
312
+ ```
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`
314
+ ```ruby
315
+ # Default setup
316
+ 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)
322
+ end
323
+ end
324
+ end
325
+ ```
326
+ In Rails 6.1 `ActiveModel::Error` was introdused and previous setup is not needed, second argument is used instead
327
+ ```ruby
328
+ errors.add(:base, :unprocessable_entity, 'error')
329
+ ```
27
330
 
28
331
  ## Development
29
332
 
data/Rakefile CHANGED
@@ -1,6 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task default: :spec
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
File without changes
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'steel_wheel'
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates operations files
3
+
4
+ Example:
5
+ rails generate steel_wheel:application_handler
6
+
7
+ This will create:
8
+ app/handlers/application_handler.rb
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SteelWheel
4
+ class ApplicationHandlerGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def copy_files
8
+ empty_directory Pathname.new('app/handlers')
9
+ template 'handler_template.rb', 'app/handlers/application_handler.rb'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationHandler < SteelWheel::Handler
4
+ end
File without changes
@@ -1,16 +1,16 @@
1
- require_relative '../generic_generator'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module SteelWheel
4
- class CommandGenerator < GenericGenerator
5
- setup_templates_root('command/templates')
4
+ class CommandGenerator < Rails::Generators::NamedBase
5
+ source_root File.expand_path('templates', __dir__)
6
6
 
7
- on_revoke do
8
- template 'command_template.rb', "app/commands/#{file_path}_command.rb"
9
- end
10
-
11
- on_invoke do
12
- empty_directory Pathname.new('app/commands').join(*class_path)
13
- template 'command_template.rb', "app/commands/#{file_path}_command.rb"
7
+ def copy_files
8
+ if behavior == :revoke
9
+ template 'command_template.rb', "app/handlers/#{file_path}_handler/command.rb"
10
+ elsif behavior == :invoke
11
+ empty_directory Pathname.new('app/commands').join(*class_path)
12
+ template 'command_template.rb', "app/handlers/#{file_path}_handler/command.rb"
13
+ end
14
14
  end
15
15
  end
16
16
  end
@@ -1,3 +1,5 @@
1
- class <%= class_name %>Command < SteelWheel::Command
1
+ class <%= class_name %>Handler
2
+ class Command < base_class_for(:command)
2
3
 
4
+ end
3
5
  end
File without changes
@@ -1,16 +1,16 @@
1
- require_relative '../generic_generator'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module SteelWheel
4
- class HandlerGenerator < GenericGenerator
5
- setup_templates_root('handler/templates')
4
+ class HandlerGenerator < Rails::Generators::NamedBase
5
+ source_root File.expand_path('templates', __dir__)
6
6
 
7
- on_revoke do
8
- template 'handler_template.rb', "app/handlers/#{file_path}_handler.rb"
9
- end
10
-
11
- on_invoke do
12
- empty_directory Pathname.new('app/handlers').join(*class_path)
13
- template 'handler_template.rb', "app/handlers/#{file_path}_handler.rb"
7
+ def copy_files
8
+ if behavior == :revoke
9
+ template 'handler_template.rb', "app/handlers/#{file_path}_handler.rb"
10
+ elsif behavior == :invoke
11
+ empty_directory Pathname.new('app/handlers').join(*class_path)
12
+ template 'handler_template.rb', "app/handlers/#{file_path}_handler.rb"
13
+ end
14
14
  end
15
15
  end
16
16
  end
@@ -1,15 +1,21 @@
1
1
  class <%= class_name %>Handler < ApplicationHandler
2
- params_input do
2
+ define do
3
+ params do
3
4
 
4
- end
5
+ end
6
+
7
+ query do
8
+
9
+ end
5
10
 
6
- command_stage do
7
- def call
8
- # NOOP
11
+ command do
12
+ def call(*)
13
+ # NOOP
14
+ end
9
15
  end
10
16
  end
11
17
 
12
- def on_success
13
- flow.call
18
+ def on_success(flow)
19
+ flow.call(flow)
14
20
  end
15
21
  end
File without changes
@@ -1,16 +1,16 @@
1
- require_relative '../generic_generator'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module SteelWheel
4
- class ParamsGenerator < GenericGenerator
5
- setup_templates_root('params/templates')
4
+ class ParamsGenerator < Rails::Generators::NamedBase
5
+ source_root File.expand_path('templates', __dir__)
6
6
 
7
- on_revoke do
8
- template 'params_template.rb', "app/params/#{file_path}_params.rb"
9
- end
10
-
11
- on_invoke do
12
- empty_directory Pathname.new('app/params').join(*class_path)
13
- template 'params_template.rb', "app/params/#{file_path}_params.rb"
7
+ def copy_files
8
+ if behavior == :revoke
9
+ template 'params_template.rb', "app/handlers/#{file_path}_handler/params.rb"
10
+ elsif behavior == :invoke
11
+ empty_directory Pathname.new('app/params').join(*class_path)
12
+ template 'params_template.rb', "app/handlers/#{file_path}_handler/params.rb"
13
+ end
14
14
  end
15
15
  end
16
16
  end
@@ -1,3 +1,5 @@
1
- class <%= class_name %>Params < SteelWheel::Params
1
+ class <%= class_name %>Handler
2
+ class Params < base_class_for(:params)
2
3
 
4
+ end
3
5
  end
File without changes
@@ -1,16 +1,16 @@
1
- require_relative '../generic_generator'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module SteelWheel
4
- class CommandGenerator < GenericGenerator
5
- setup_templates_root('query/templates')
4
+ class QueryGenerator < Rails::Generators::NamedBase
5
+ source_root File.expand_path('templates', __dir__)
6
6
 
7
- on_revoke do
8
- template 'query_template.rb', "app/queries/#{file_path}_query.rb"
9
- end
10
-
11
- on_invoke do
12
- empty_directory Pathname.new('app/queries').join(*class_path)
13
- template 'query_template.rb', "app/queries/#{file_path}_query.rb"
7
+ def copy_files
8
+ if behavior == :revoke
9
+ template 'query_template.rb', "app/handlers/#{file_path}_handler/query.rb"
10
+ elsif behavior == :invoke
11
+ empty_directory Pathname.new('app/queries').join(*class_path)
12
+ template 'query_template.rb', "app/handlers/#{file_path}_handler/query.rb"
13
+ end
14
14
  end
15
15
  end
16
16
  end
@@ -1,3 +1,5 @@
1
- class <%= class_name %>Query < SteelWheel::Query
1
+ class <%= class_name %>Handler
2
+ class Query < base_class_for(:query)
2
3
 
4
+ end
3
5
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SteelWheel
4
+ # Base class for commands
2
5
  class Command
3
6
  include Memery
4
7
  include ActiveModel::Validations
@@ -8,10 +11,13 @@ module SteelWheel
8
11
  end
9
12
 
10
13
  def http_status
11
- errors.keys.first
14
+ return :ok if errors.empty?
15
+ return errors.keys.first unless defined?(ActiveModel::Error)
16
+
17
+ errors.map(&:type).first
12
18
  end
13
19
 
14
- def call
20
+ def call(*)
15
21
  # NOOP
16
22
  end
17
23
  end