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 +7 -0
- data/.rspec +1 -0
- data/.standard.yml +6 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +449 -0
- data/Rakefile +14 -0
- data/app/controllers/wicked/pipeline/base_steps_controller.rb +191 -0
- data/lib/wicked/pipeline/base_pipeline.rb +126 -0
- data/lib/wicked/pipeline/base_step.rb +304 -0
- data/lib/wicked/pipeline/engine.rb +27 -0
- data/lib/wicked/pipeline/readonly_step.rb +23 -0
- data/lib/wicked/pipeline/version.rb +7 -0
- data/lib/wicked/pipeline.rb +37 -0
- data/sig/wicked/pipeline.rbs +5 -0
- data/wicked-pipeline.gemspec +35 -0
- metadata +157 -0
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
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/CHANGELOG.md
ADDED
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]
|