wicked-pipeline 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 315fc6476825e5f4e336eb28b3951fdc6851902919916547c0a3f046da73c353
4
+ data.tar.gz: 1af17136066174c31e7bed32aa43e477f1b1bbdc1c1e0775a6eb4d256806ac37
5
+ SHA512:
6
+ metadata.gz: bd19625bc5ee76db912dc057f8f2425c23e423ca9775253701a45884f3a4c734fc013e8dac350cd40df4a000c9c96f3bf3399991f68975cfada8a277a5b23c4f
7
+ data.tar.gz: b925b48b9a027cc8cc18b346fe92be23e036c85e44252082768248771daad59b2b98a384eeab631c42d90e598ae31fb1884dca9d38b21cc42e895f9b100aa8ae
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,6 @@
1
+ ruby_version: 2.6
2
+ parallel: true
3
+ format: progress
4
+
5
+ ignore:
6
+ - "spec/rails_app/db/schema.rb"
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ [Unreleased]
2
+ ------------
3
+
4
+ [0.1.0] - 2022-04-15
5
+ --------------------
6
+
7
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
+
6
+ gemspec
7
+
8
+ gem "rails", ">= 6.1", "< 7"
9
+ gem "rake", "~> 13.0"
10
+ gem "standard", "~> 1.3"
11
+ gem "yard", "~> 0.9"
12
+
13
+ group :development, :test do
14
+ gem "rspec-rails", "~> 5.0"
15
+ gem "sqlite3"
16
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Robert Audi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,449 @@
1
+ Wicked::Pipeline
2
+ ================
3
+
4
+ A step by step pipeline is comprised of multiple "blocks" that will be described here:
5
+
6
+ - Step objects for each step
7
+ - A step pipeline class that encapsulates all the steps in a pipeline
8
+ - A step controller
9
+
10
+ Table of Contents
11
+ -----------------
12
+
13
+ - [Installation](#installation)
14
+ - [Step objects](#step-objects)
15
+ - [Attributes](#attributes)
16
+ - [Validations](#validations)
17
+ - [Blocking](#blocking)
18
+ * [Blocking reason](#blocking-reason)
19
+ - [Step pipelines](#step-pipelines)
20
+ - [Steps controllers](#steps-controllers)
21
+ - [Routes](#routes)
22
+ - [Nested routes](#nested-routes)
23
+ - [Actions](#actions)
24
+ - [Flash messages](#flash-messages)
25
+ - [Views](#views)
26
+ - [Forms](#forms)
27
+ - [Breadcrumbs](#breadcrumbs)
28
+
29
+ Installation
30
+ ------------
31
+
32
+ Install the gem and add to the application's Gemfile by executing:
33
+
34
+ ```console
35
+ $ bundle add wicked-pipeline
36
+ ```
37
+
38
+ If bundler is not being used to manage dependencies, install the gem by executing:
39
+
40
+ ```console
41
+ $ gem install wicked-pipeline
42
+ ```
43
+
44
+ Step objects
45
+ ------------
46
+
47
+ A step object is very similar to a form object. It takes a "resource" (the `ActiveRecord` model) and optional params as arguments and will contain attribute definitions, validations for the step, a list of permitted params and a `#save` method. The validations are specific to the step but the resource will also be validated before being saved.
48
+
49
+ Step objects are subclasses of the `BaseStep` class, and they live in the `app/steps` directory. Here are the rules that must be respected in a step object:
50
+
51
+ - The name of a step class must end with `Step`
52
+ - Attributes must be defined using the `ActiveModel::Attributes` API (more on that later)
53
+ * Nested attributes must **not** be defined as attributes in the step object.
54
+ - The class must implement the `permitted_params` method which returns an array of attributes.
55
+ - If the `#save` method is overridden, `super` must be called and its value must be used.
56
+ * The `#save` method **must** return a boolean value.
57
+
58
+ The only method that needs to be implemented is the `permitted_params` private method, everything else is optional. Here is the most basic step object that can exist:
59
+
60
+ ```ruby
61
+ module Users
62
+ class ProfileStep < ::BaseStep
63
+ private
64
+
65
+ def permitted_params
66
+ []
67
+ end
68
+ end
69
+ end
70
+ ```
71
+
72
+ That's it!
73
+
74
+ As mentioned before, step objects require a resource:
75
+
76
+ ```ruby
77
+ Users::ProfileStep.new(User.last)
78
+ ```
79
+
80
+ ### Attributes
81
+
82
+ The attributes of a step object are defined using the `.attribute` method. The first argument is the name of the attribute, and the second argument is the type of the attribute. The type argument is optional but it **must** be specified for boolean attributes.
83
+
84
+ To define a String attributes as an Array, the second argument must be `array: true` and the `:string` type **must not** be specified.
85
+
86
+ ```ruby
87
+ module Users
88
+ class ProfileStep < ::BaseStep
89
+ attribute :email
90
+ attribute :first_name
91
+ attribute :last_name
92
+ attribute :is_us_citizen, :boolean
93
+ attribute :investment_goals, array: true
94
+
95
+ private
96
+
97
+ def permitted_params
98
+ %i[email first_name last_name is_us_citizen investment_goals]
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ The attributes **must** be in the list of permitted parameters!
105
+
106
+ ### Validations
107
+
108
+ Validations are used the same way as in `ActiveRecord` models. One exception is the uniqueness validation which is **not** available in step objects.
109
+
110
+ _Hint: A custom validation method must be used for uniqueness validations, but usually uniqueness validations should be defined in the model._
111
+
112
+ ```ruby
113
+ module Users
114
+ class ProfileStep < ::BaseStep
115
+ # ...
116
+
117
+ validates_presence_of :email, :first_name, :last_name
118
+ validates :is_us_citizen, inclusion: { in: [true, false] }
119
+ validate :full_name_must_not_be_too_long
120
+
121
+ private
122
+
123
+ def full_name_must_not_be_too_long
124
+ unless "#{first_name} #{last_name}".length <= 255
125
+ errors.add(:base, :too_long, count: 255)
126
+ end
127
+ end
128
+
129
+ # ...
130
+ end
131
+ end
132
+ ```
133
+
134
+ Custom validation errors must be added to the step object **not** the resource itself, they will be merged into the resource's errors automatically.
135
+
136
+ ### Blocking
137
+
138
+ A blocking step will short-circuit a pipeline. In other words, all step following a blocking step will be inaccessible.
139
+
140
+ A step can be marked as "blocking" by overriding the `blocking?` predicate method:
141
+
142
+ ```ruby
143
+ module Users
144
+ class ProfileStep < ::BaseStep
145
+ attribute :first_name
146
+ attribute :is_us_citizen, :boolean
147
+
148
+ def blocking?
149
+ first_name == "John" || is_us_citizen
150
+ end
151
+
152
+ # ...
153
+ end
154
+ end
155
+ ```
156
+
157
+ Since the `blocking?` method is a predicate method, it **must** return a boolean value.
158
+
159
+ #### Blocking reason
160
+
161
+ To specify a reason why the step is marked as blocking, the `blocking_reason` method should be overridden:
162
+
163
+ ```ruby
164
+ module Users
165
+ class ProfileStep < ::BaseStep
166
+ attribute :first_name
167
+ attribute :is_us_citizen, :boolean
168
+
169
+ def blocking?
170
+ first_name == "John" || is_us_citizen
171
+ end
172
+
173
+ def blocking_reason
174
+ return nil unless blocking?
175
+
176
+ if first_name == "John"
177
+ "Too cool for school"
178
+ elsif is_us_citizen
179
+ "Vive la France"
180
+ end
181
+ end
182
+
183
+ # ...
184
+ end
185
+ end
186
+ ```
187
+
188
+ Step pipelines
189
+ --------------
190
+
191
+ A step pipeline class is a subclass of the `BasePipeline` class and live in `app/steps`. At the most basic level should contain a list of step classes.
192
+
193
+ **Note:** Step pipeline should only be used with step objects!
194
+
195
+ ```ruby
196
+ class UserAccountPipeline < BasePipeline
197
+ def steps
198
+ [
199
+ User::ProfileStep,
200
+ User::BankingInfoStep,
201
+ User::ObjectivesStep
202
+ ]
203
+ end
204
+ end
205
+ ```
206
+
207
+ The order of the steps will be used in the controller/views, so it's easy to reorder steps at any time.
208
+
209
+ Steps metadata can be accessed using the pipeline. This includes the name of a step, whether or not it is valid and whether or not it is accessible:
210
+
211
+ ```ruby
212
+ UserAccountPipeline.metadata(User.last)
213
+ #=> {
214
+ # profile: { valid: true, accessible: true },
215
+ # banking_info: { valid: false, accessible: true },
216
+ # objectives: { valid: false, accessible: false }
217
+ # }
218
+ ```
219
+
220
+ A step is accessible if the previous step is valid and accessible. The first step is always accessible.
221
+
222
+ Finally, pipelines are also used to check if all steps are valid for a given resource:
223
+
224
+ ```ruby
225
+ UserAccountPipeline.valid?(User.last)
226
+ ```
227
+
228
+ Steps controllers
229
+ -----------------
230
+
231
+ A steps controller is a subclass of `BaseStepsController`, it can have any name and can be placed anywhere under the `app/controllers` directory. Unlike a regular controller, the `:id` parameter references the current step **not** the ID of a resource. For this reason, the name of the resource ID parameter must be specified using the `#resource_param_name` private method.
232
+
233
+ Steps controllers **must** implement the following private methods:
234
+
235
+ - `#resource_param_name`: This method must return the name of the resource ID parameter (eg: `:user_id`).
236
+ - `#steps_pipeline`: This method must return the pipeline class that will be used in the steps controller (eg: `UserAccountPipeline`)
237
+ - `#find_resource`: This method takes care of finding the resource object from the database.
238
+
239
+ ```ruby
240
+ class UsersController < BaseStepsController
241
+ # ...
242
+
243
+ private
244
+
245
+ def resource_param_name
246
+ :user_id
247
+ end
248
+
249
+ def steps_pipeline
250
+ UserAccountPipeline
251
+ end
252
+
253
+ def find_resource
254
+ User.find(params[resource_param_name])
255
+ end
256
+ end
257
+ ```
258
+
259
+ **Rules to follow**
260
+
261
+ - The `#find_resource` method **must not** set any instance variables.
262
+ - The `#find_resource` method must only retrieve the record associated with the resource ID (`#includes` is allowed), **not** a collection of records.
263
+ - The `#find_resource` method must return the record.
264
+
265
+ ### Routes
266
+
267
+ The `param` option must be specified when defining resource routes:
268
+
269
+ ```ruby
270
+ resources :users, only: [:show, :update], param: :user_id
271
+ ```
272
+
273
+ **DO NOT** use nested routes with the step routes.
274
+
275
+ ### Nested routes
276
+
277
+ When the controller has an `index` action, nested routes can be defined in the following way:
278
+
279
+ ```ruby
280
+ resources :users, only: [:show, :update], param: :user_id
281
+
282
+ resources :users, only: [:index] do
283
+ resources :profiles
284
+ end
285
+ ```
286
+
287
+ When the controller doesn't have an `index` action, nested routes should be defined in the following way instead:
288
+
289
+ ```ruby
290
+ resources :users, only: [:show, :update], param: :user_id
291
+
292
+ scope path: "users/:user_id" do
293
+ resources :profiles
294
+ end
295
+ ```
296
+
297
+ ### Actions
298
+
299
+ A step controller has the `show` and `update` actions. **It cannot have the `new`, `edit` and `create` actions!** The `index` and `destroy` actions can be implemented but they will be independent of the pipeline.
300
+
301
+ Both action must call `super` with a block and set the `@step_processor` instance variable inside of that block:
302
+
303
+ ```ruby
304
+ class UsersController < BaseStepsController
305
+ def show
306
+ super do
307
+ @user = find_resource
308
+ @step_processor = step_class.new(@user)
309
+ end
310
+ end
311
+
312
+ def update
313
+ super do
314
+ @user = find_resource
315
+ @step_processor = step_class.new(@user, params.require(:user))
316
+ end
317
+ end
318
+
319
+ # ...
320
+ end
321
+ ```
322
+
323
+ **Notes:**
324
+
325
+ - The `step_class` method returns the step object class associated with the current step.
326
+ - Other instance variables can be set before calling `super` or inside the block.
327
+
328
+ **Rules to follow:**
329
+
330
+ - **DO NOT** implement the `new`, `edit` or `create` actions.
331
+ - **Always** call `super` with a block in the `show` and `update` actions.
332
+ - **Always** set the `@step_processor` instance variable inside the `super` block.
333
+ - **DO NOT** call `#save` manually, this will be done automatically.
334
+ - **DO NOT** set the the following instance variable:
335
+ - `@step`
336
+ - `@next_step`
337
+ - `@previous_step`
338
+
339
+ ### Flash messages
340
+
341
+ Flash messages can be set manually **after** calling `super`. Step objects have a `#saved?` method which can be used to verify that it was successfully saved. The method should be used before setting a flash messages:
342
+
343
+ ```ruby
344
+ class UsersController < BaseStepsController
345
+ # ...
346
+
347
+ def update
348
+ super do
349
+ @user = find_resource
350
+ @step_processor = step_class.new(@user, params.require(:user))
351
+ end
352
+
353
+ if @step_processor.saved?
354
+ flash[:success] = t(".success")
355
+ end
356
+ end
357
+
358
+ # ...
359
+ end
360
+ ```
361
+
362
+ ### Views
363
+
364
+ There must be a view for each step, **not** a partial, it must have the name of the step and it should live in the root directory of the controller's view directory:
365
+
366
+ ```
367
+ app
368
+ └── views/
369
+ └── users/
370
+ ├── banking_info.html.erb
371
+ ├── objectives.html.erb
372
+ └── profile.html.erb
373
+ ```
374
+
375
+ **Rules to follow**
376
+
377
+ - **DO NOT** create a view for the `show` action.
378
+ - **DO NOT** create views named `new` or `edit`.
379
+
380
+ ### Forms
381
+
382
+ The form in step view must be declared in the following way:
383
+
384
+ ```erb
385
+ <%= form_for @user, url: step_path, method: :patch do |f| %>
386
+ <% # ... %>
387
+ <% end %>
388
+ ```
389
+
390
+ **Notes**
391
+
392
+ - The `step_path` method refers to the path of the current step. It can also be used to get the path of a specific step:
393
+
394
+ ```ruby
395
+ step_path(step_name: "objectives")
396
+ ```
397
+
398
+ **Rules to follow**
399
+
400
+ - The resource object must be passed to `form_for`, **not** the step processor
401
+ - The form's method must be either `:put` or `:patch`
402
+
403
+ ### Breadcrumbs
404
+
405
+ Step controllers provide a extended version of their pipeline's steps metadata with the following added info:
406
+
407
+ - `:url`: The URL of a step
408
+ - `:active`: Whether or not a step is currently active
409
+
410
+ This information is made to be used to build breadcrumbs. Here is a basic way to use steps metadata to build breadcrumbs:
411
+
412
+ ```erb
413
+ <nav aria-label="breadcrumb" class="p-0">
414
+ <ol class="breadcrumb p-0">
415
+ <% steps_metadata.each do |step_name, step_metadata| %>
416
+ <li class="<%= "active-step" if step_metadata[:active] %> <%= "user-#{step_name.dasherize}-step" %> py-2 px-4">
417
+ <% if step_metadata[:accessible] %>
418
+ <%= link_to step_metadata[:url] do %>
419
+ <i class="far <%= step_metadata[:valid] ? "fa-check-square" : "fa-square" %> mr-1"></i>
420
+ <%== t ".#{step_name}" %>
421
+ <% end %>
422
+ <% else %>
423
+ <span>
424
+ <i class="far fa-square mr-1"></i>
425
+ <%== t ".#{step_name}" %>
426
+ </span>
427
+ <% end %>
428
+ </li>
429
+ <% end %>
430
+ </ol>
431
+ </nav>
432
+ ```
433
+
434
+ Development
435
+ -----------
436
+
437
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
438
+
439
+ To install this gem onto your local machine, run `bundle exec rake install`.
440
+
441
+ Contributing
442
+ ------------
443
+
444
+ Bug reports and pull requests are welcome on GitHub at https://github.com/RobertAudi/wicked-pipeline.
445
+
446
+ License
447
+ -------
448
+
449
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "standard/rake"
6
+ require "yard"
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ YARD::Rake::YardocTask.new do |t|
11
+ t.files = ["lib/**/*.rb"]
12
+ end
13
+
14
+ task default: [:standard, :spec]