pathway 0.0.20 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +483 -26
- data/lib/pathway/plugins/sequel_models.rb +0 -4
- data/lib/pathway/plugins/{authorization.rb → simple_auth.rb} +1 -1
- data/lib/pathway/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '074681d0fda0240e14f6a8157716f872c6af54a8'
|
4
|
+
data.tar.gz: 3405a3f3c338bda484efa7b9221601d24c2dd35e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d0f049e84677b12d58d4e53d7781ae80bba9e6859c9d25f46f10e4e7c25c3eea0f13cd1c6ca436a20e3ad910f3b9d31b3c21c8a6e47e07a8382fab0268524cd1
|
7
|
+
data.tar.gz: 9df6468ce430ea9c640ad088f8b0e1e37ff69379b30d9002bca705bd5c3b989648a221b2d8d95939e50b94168d8217adbfb8b15b845546829ea0abeb68af68e6
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
## [0.4.0] - 2017-10-31
|
2
|
+
### Changed
|
3
|
+
- Renamed `:authorization` plugin to `:simple_auth`
|
4
|
+
|
5
|
+
### Removed
|
6
|
+
- Removed `build_model_with` method from `:sequel_models` plugin
|
7
|
+
|
8
|
+
### Added
|
9
|
+
- New documentation for core functionality and plugins
|
10
|
+
|
11
|
+
## [0.3.0] - 2017-10-31 [YANKED]
|
12
|
+
|
13
|
+
## [0.2.0] - 2017-10-31 [YANKED]
|
14
|
+
|
15
|
+
## [0.1.0] - 2017-10-31 [YANKED]
|
16
|
+
|
17
|
+
## [0.0.20] - 2017-10-17
|
18
|
+
### Changed
|
19
|
+
- Renamed options `key:` and `column:` to `using` and `search_by`, for `:fetch_model` step, at `:sequel_models` plugin
|
20
|
+
|
21
|
+
### Added
|
22
|
+
- Added new option `to:` for overriding where to store the result, for `:fetch_model` step, at `:sequel_models` plugin
|
23
|
+
|
24
|
+
## [0.0.19] - 2017-10-17
|
25
|
+
### Removed
|
26
|
+
- Removed `Error#error_type` (use `Error#type` instead)
|
27
|
+
- Removed `Error#error_message` (use `Error#message` instead)
|
28
|
+
- Removed `Error#errors` (use `Error#details` instead)
|
29
|
+
|
30
|
+
## [0.0.18] - 2017-10-08
|
31
|
+
### Changed
|
32
|
+
- Changed `:sequel_models` default value for `search_by:` option from `:id` to the model's primary key
|
data/README.md
CHANGED
@@ -28,28 +28,27 @@ Pathway helps you separate your business logic from the rest of your application
|
|
28
28
|
The main concept Pathway relies upon to build domain logic modules is the operation, this important concept will be explained in detail in the following sections.
|
29
29
|
|
30
30
|
|
31
|
-
Pathway also aims to be easy to use, stay lightweight and
|
31
|
+
Pathway also aims to be easy to use, stay lightweight and extensible (by the use of plugins), avoid unnecessary dependencies, keep the core classes clean from monkey patching and help yielding an organized and uniform codebase.
|
32
32
|
|
33
33
|
## Usage
|
34
34
|
|
35
35
|
### Core API and concepts
|
36
36
|
|
37
|
-
As mentioned earlier the operation is a crucial concept Pathway leverages upon. Operations not only structure your code (using steps as will be explained
|
38
|
-
|
37
|
+
As mentioned earlier the operation is a crucial concept Pathway leverages upon. Operations not only structure your code (using steps as will be explained later) but also express meaningful business actions. Operations can be thought as use cases too: they represent an activity -to be perform by an actor interacting with the system- which should be understandable by anyone familiar with the business regardless of their technical expertise.
|
39
38
|
|
40
39
|
Operations should ideally don't contain any business rules but instead orchestrate and delegate to other more specific subsystems and services. The only logic present then should be glue code or any adaptations required to make interactions with the inner system layers possible.
|
41
40
|
|
42
41
|
#### Function object protocol (the `call` method)
|
43
42
|
|
44
43
|
Operations works as function objects, they are callable and hold no state, as such, any object that responds to `call` and returns a result object can be a valid operation and that's the minimal protocol they needs to follow.
|
45
|
-
The result object must follow its
|
44
|
+
The result object must follow its protocol as well (and a helper class is provided for that end) but we'll talk about that in a minute.
|
46
45
|
|
47
46
|
Let's see an example:
|
48
47
|
|
49
48
|
```ruby
|
50
49
|
class MyFirstOperation
|
51
|
-
def call(
|
52
|
-
result = Repository.create(
|
50
|
+
def call(input)
|
51
|
+
result = Repository.create(input)
|
53
52
|
|
54
53
|
if result.valid?
|
55
54
|
Pathway::Result.success(result)
|
@@ -65,7 +64,6 @@ if result.success?
|
|
65
64
|
else
|
66
65
|
puts "Error: #{result.error}"
|
67
66
|
end
|
68
|
-
|
69
67
|
```
|
70
68
|
|
71
69
|
Note first we are not inheriting from any class nor including any module. This won't be the case in general as `pathway` provides classes to help build your operations, but it serves to illustrate how little is needed to implement one.
|
@@ -73,10 +71,10 @@ Note first we are not inheriting from any class nor including any module. This w
|
|
73
71
|
Also, let's ignore the specifics about `Repository.create(...)`, we just need to know that is some backend service which can return a value.
|
74
72
|
|
75
73
|
|
76
|
-
We
|
77
|
-
And that
|
74
|
+
We then define a `call` method for the class. It only checks if the result is available and then wrap it into a successful `Result` object when is ok, or a failing one when is not.
|
75
|
+
And that is all is needed, you can then call the operation object, check whether it was completed correctly with `success?` and get the resulting value.
|
78
76
|
|
79
|
-
By following this protocol, you will be able to uniformly apply the same pattern on every HTTP endpoint (or whatever means your app has to communicates with the outside world). The upper layer of the application
|
77
|
+
By following this protocol, you will be able to uniformly apply the same pattern on every HTTP endpoint (or whatever means your app has to communicates with the outside world). The upper layer of the application will offload all the domain logic to the operation and only will need to focus on the HTTP transmission details.
|
80
78
|
|
81
79
|
Maintaining always the same operation protocol will also be very useful when composing them.
|
82
80
|
|
@@ -85,20 +83,18 @@ Maintaining always the same operation protocol will also be very useful when com
|
|
85
83
|
|
86
84
|
As should be evident by now an operation should always return either a successful or failed result. This concepts are represented by following a simple protocol, which `Pathway::Result` subclasses comply.
|
87
85
|
|
88
|
-
As we seen before, by querying `success?` on the result we can see if the operation we just ran went well, you can also call to `failure?` for a negated version.
|
89
|
-
|
90
|
-
The actual result value produced by the operation is be accessible at the `value` method and the error description (if there's any) at `error` when the operation fails.
|
91
|
-
|
92
|
-
To return wrapped values or errors from your operation you can must call to `Pathway::Result.success(value)` or `Pathway::Result.failure(error)`.
|
86
|
+
As we seen before, by querying `success?` on the result we can see if the operation we just ran went well, or you can also call to `failure?` for a negated version.
|
93
87
|
|
88
|
+
The actual result value produced by the operation is accessible at the `value` method and the error description (if there's any) at `error` when the operation fails.
|
89
|
+
To return wrapped values or errors from your operation you must call to `Pathway::Result.success(value)` or `Pathway::Result.failure(error)`.
|
94
90
|
|
95
91
|
It is worth mentioning that when you inherit from `Pathway::Operation` you'll have helper methods at your disposal to create result objects easier, for instance the previous section's example could be written as follows:
|
96
92
|
|
97
93
|
|
98
94
|
```ruby
|
99
95
|
class MyFirstOperation < Pathway::Operation
|
100
|
-
def call(
|
101
|
-
result = Repository.create(
|
96
|
+
def call(input)
|
97
|
+
result = Repository.create(input)
|
102
98
|
|
103
99
|
result.valid? ? success(result) : failure(:create_error)
|
104
100
|
end
|
@@ -106,30 +102,491 @@ end
|
|
106
102
|
```
|
107
103
|
|
108
104
|
#### Error objects
|
109
|
-
|
105
|
+
|
106
|
+
`Pathway::Error` is a helper class to represent the error description from an failed operation execution (and can be used also for pattern matching as we'll see later).
|
107
|
+
It's use is completely optional, but provides you with a basic schema to communicate what when wrong. You can instantiate it by calling `new` on the class itself or using the helper method `error` provided in the operation class:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class CreateNugget < Pathway::Operation
|
111
|
+
def call(input)
|
112
|
+
validation = Form.call(input)
|
113
|
+
|
114
|
+
if validation.ok?
|
115
|
+
success(Nugget.create(validation.values))
|
116
|
+
else
|
117
|
+
error(type: :validation, message: 'Invalid input', details: validation.errors)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
As you can see `error(...)` expects `type:`, `message:` and `details` keyword arguments; `type:` is the only mandatory, the other ones can be omitted and have default values. Also `type` should be a `Symbol`, `message:` a `String` and `details:` can be a `Hash` or any other structure you see fit.
|
124
|
+
|
125
|
+
You then have accessors available on the error object to get the values back:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
result = CreateNugget.new.call(foo: 'foobar')
|
129
|
+
if result.failure?
|
130
|
+
puts "#{result.error.type} error: #{result.error.message}"
|
131
|
+
end
|
132
|
+
|
133
|
+
```
|
134
|
+
|
135
|
+
Mind you, `error(...)` creates an `Error` object wrapped into a `Pathway::Failure` so you don't have to do it yourself.
|
136
|
+
If you decide to use `Pathway::Error.new(...)` directly, the expected arguments will be the same, but you will have to wrap the object before returning it.
|
137
|
+
|
138
|
+
#### Initialization context
|
139
|
+
|
140
|
+
It was previously mentioned that operations should work like functions, that is, they don't hold state and you should be able to execute the same instance all the times you need, on the other hand there will be some values that won't change during the operation life time and won't make sense to pass as `call` parameters, you can provide these values on initialization as context data.
|
141
|
+
|
142
|
+
Context data can be thought as 'request data' on an HTTP endpoint, values that aren't global but won't change during the executing of the request. Examples of this kind of data are the current user, the current device, a CSRF token, other configuration parameters, etc. You will want to pass this values on initialization, and probably pass them along to other operations down the line.
|
143
|
+
|
144
|
+
You must define your initializer to accept a `Hash` with this values, which is what every operation is expected to do, but as before, when inheriting from `Operation` you have the helper class method `context` handy to make it easier for you:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class CreateNugget < Pathway::Operation
|
148
|
+
context :current_user, notify: false
|
149
|
+
|
150
|
+
def call(input)
|
151
|
+
validation = Form.call(input)
|
152
|
+
|
153
|
+
if validation.valid?
|
154
|
+
nugget = Nugget.create(owner: current_user, **validation.values)
|
155
|
+
|
156
|
+
Notifier.notify(:new_nugget, nugget) if @notify
|
157
|
+
success(nugget)
|
158
|
+
else
|
159
|
+
error(type: :validation, message: 'Invalid input', details: validation.errors)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
op = CreateNugget.new(current_user: user)
|
166
|
+
op.call(foo: 'foobar')
|
167
|
+
```
|
168
|
+
|
169
|
+
On the example above `context` is defining `:current_user` as a mandatory argument (it will raise an error if not provided) and `:notify` as an optional config argument, since it has a default value. Note that any extra non-defined value provided will be simply ignored.
|
170
|
+
|
171
|
+
Both of these parameters are available through accessors (and instance variables) inside the operation. Also there is a `context` private method you use to get all the initialization values as a frozen hash, in order to pass then along easily.
|
172
|
+
|
173
|
+
#### Alternative invocation syntax
|
174
|
+
|
175
|
+
If you don't care about keeping the operation instance around you can execute the operation directly on the class. To do so, use `call` with the initialization context first and then the remaining parameters:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
user = User.first(session[:current_user_id])
|
179
|
+
context = { current_user: user }
|
180
|
+
|
181
|
+
CreateNugget.call(context, params[:nugget]) # Using 'call' on the class
|
182
|
+
```
|
183
|
+
|
184
|
+
Also you have Ruby's alternative syntax to invoke the `call` method: `CreateNugget.(context, params[:nugget])`. On any case you'll get the operation result like when invoking `call` on the operation's instance.
|
185
|
+
|
186
|
+
Mind you that a context must always be provided for this syntax, if you don't need any initialization use an empty hash.
|
187
|
+
|
188
|
+
There's also third way to execute an operation, made available through a plugin, and will be explained later.
|
189
|
+
|
110
190
|
#### Steps
|
111
191
|
|
112
192
|
Finally the steps, these are the heart of the `Operation` class and the main reason you will want to inherit your own classes from `Pathway::Operation`.
|
113
193
|
|
114
|
-
|
115
|
-
|
194
|
+
So far we know that every operation needs to implement a `call` method and return a valid result object, `pathway` provides another option: the `process` block DSL, this method will define `call` behind the scenes for us, while also providing a way to define a business oriented set of steps to describe our operation's behavior.
|
195
|
+
|
196
|
+
Every step should be cohesive and focused on a single responsibly, ideally by offloading work to other subsystems. Designing steps this way is the developer's responsibility, but is made much simpler by the use of custom steps provided by plugins as we'll see later.
|
197
|
+
|
198
|
+
##### Process DSL
|
199
|
+
|
200
|
+
Lets start by showing some actual code:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
# ...
|
204
|
+
# Inside an operation class body...
|
205
|
+
process do
|
206
|
+
step :authorize
|
207
|
+
step :validate
|
208
|
+
set :create_nugget, to: :nugget
|
209
|
+
step :notify
|
210
|
+
end
|
211
|
+
# ...
|
212
|
+
```
|
213
|
+
|
214
|
+
To define your `call` method using the DSL just call to `process` and pass a block, inside it the DSL will be available.
|
215
|
+
Each `step` (or `set`) call is referring to a method inside the operation class, superclasses or available through a plugin, these methods will be eventually invoked on `call`.
|
216
|
+
All of the steps constitute the operation use case and follow a series of conventions in order to carry the process state along the execution process.
|
217
|
+
|
218
|
+
When you run the `call` method, the auto-generated code will save the provided argument at the `input` key within the execution state. Subsequent steps will receive this state and will be able to update it, to set the result value or and auxiliary key to communicate with the next steps on the execution path.
|
219
|
+
|
220
|
+
Each step (as the operation as whole) can succeed of fail, when the latter happens execution is halted, and the operation `call` method returns immediately.
|
221
|
+
To signal a failure you must return a `failure(...)` or `error(...)` in the same fashion as when defining `call` directly.
|
222
|
+
|
223
|
+
If you return a `success(...)` or anything that's not a failure the execution carries on but the value is ignored. If you want to save the result value, you must use `set` instead of `step` at the process block, that will save your wrapped value, into the key provided at `to:`.
|
224
|
+
Also non-failure return values inside steps are automatically wrapped so you can use `success` for clarity sake but it's optional.
|
225
|
+
If you omit the `to:` keyword argument when defining a `set` step, the result key value will be used by default, but we'll explain more on that later.
|
226
|
+
|
227
|
+
##### Operation execution state
|
228
|
+
|
229
|
+
In order to operate with the execution state, every step method receives a structure representing the current state. This structure is similar to a `Hash` and responds to its key methods (`:[]`, `:[]=`, `:fetch`, `:store` and `:include?`).
|
230
|
+
|
231
|
+
When an operation is executed, before running the first step, an initial state is created by coping all the values from the initialization context (and also including `input`).
|
232
|
+
Note that these values can be replaced on later steps but it won't mutate the context object itself since is always frozen.
|
233
|
+
|
234
|
+
A state object can be splatted on method definition in the same fashion as a `Hash`, allowing to cherry pick the attributes we are interested for a given step:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
# ...
|
238
|
+
# This step only takes the values it needs and doesn't change the state.
|
239
|
+
def send_emails(user:, report:, **)
|
240
|
+
ReportMailer.send_report(user.email, report)
|
241
|
+
end
|
242
|
+
# ...
|
243
|
+
```
|
244
|
+
|
245
|
+
Note the empty double splat at the end of the parameter list, this Ruby-ism means: grab the mentioned keys and ignore all the rest. If you omit it when you have outstanding keys Ruby's `Hash` destructing will fail.
|
246
|
+
|
247
|
+
##### Successful operation result
|
248
|
+
|
249
|
+
On each step you can access or change the operation result for a successful execution.
|
250
|
+
The value will be stored at one of the attributes within the state.
|
251
|
+
By default the state `:value` key will hold the resulting value, but if you prefer to use another name you can specify it through the `result_at` operation class method.
|
252
|
+
|
253
|
+
##### Full example
|
254
|
+
|
255
|
+
Let's now go through a fully defined operation using steps:
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
class CreateNugget < Pathway::Operation
|
259
|
+
context :current_user
|
260
|
+
|
261
|
+
process do
|
262
|
+
step :authorize
|
263
|
+
step :validate
|
264
|
+
set :create_nugget
|
265
|
+
step :notify
|
266
|
+
end
|
267
|
+
|
268
|
+
result_at :nugget
|
269
|
+
|
270
|
+
def authorize(*)
|
271
|
+
unless current_user.can? :create, Nugget
|
272
|
+
error(:forbidden)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def validate(state)
|
277
|
+
validation = NuggetForm.call(state[:input])
|
278
|
+
|
279
|
+
if validation.ok?
|
280
|
+
state[:params] = validation.values
|
281
|
+
else
|
282
|
+
error(type: :validation, details: validation.errors)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def create_nugget(:params, **)
|
287
|
+
Nugget.create(owner: current_user, **params)
|
288
|
+
def
|
289
|
+
|
290
|
+
def notify(:nugget, **)
|
291
|
+
Notifier.notify(:new_nugget, nugget)
|
292
|
+
else
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
In the above example the operation will create nugget (whatever that is...). As you can see we are using the methods we mention before to indicate that we need a current user to be present `context: current_user` on initialization, a `call` method to be defined `process do ... end`, and the result value should be stored at the `:nugget` key.
|
297
|
+
|
298
|
+
Lets delve into the `process` block: it defines three steps using the `step` method and `create_nugget` using `set`, as we said before, this last step will set the result key (`:nugget`) since the `to:` keyword argument is absent.
|
299
|
+
|
300
|
+
Now, for each of the step methods:
|
301
|
+
|
302
|
+
- `:authorize` doesn't needs the state so just ignores it, then checks if the current user is allowed to perform the operation and halts the execution by returning a `:forbidden` error type if is not, otherwise does nothing and the execution goes on.
|
303
|
+
- `:validate` gets the state, checks the validity of the `:input` value which as we said is just the `call` method input, returns an `error(...)` when there's a problem, and if the validation is correct it updates the state but saving the sanitized values in `:params`. Note that the return value is `state[:params]`, but is ignored like the last one, since this method is specified using `step`.
|
304
|
+
- `:create_nugget` first takes the `:params` attribute from the state (ignoring everything else), and calls `create` on the `Nugget` model with the sanitized params and the current user. The return value is saved to the result key (`:nugget` in this case) as the step is defined using `step` without `to:`.
|
305
|
+
- `:notify` grabs the `:nugget` from the state, and simply emits a notification with it, it has no meaningful return value, so is ignored.
|
306
|
+
|
307
|
+
This example basically touches all the essential concepts needed for defining an operation class. If you can grasp it you already have a good understanding on how to implement one. There are still some very important bits to cover (like testing), and we'll tackle that on later sections.
|
308
|
+
|
309
|
+
On a final note, you may be thinking that the code could be bit less verbose; also, shouldn't very common stuff like validation or authorization be simpler to use?; and maybe, why specify the result key?, it could be possible infer it from the surrounding code. We will address all these issues on the next section by using plugins, `pathway`'s extension mechanism.
|
116
310
|
|
117
311
|
### Plugins
|
118
|
-
#### Plugin architecture
|
119
312
|
|
120
|
-
|
313
|
+
Pathway can be extended by the use of plugins. They are very similar to the one found in [Roda](http://roda.jeremyevans.net/) or [Sequel](http://sequel.jeremyevans.net/). So if you are already familiar with any of those gems you shouldn't have any problem using `pathway`'s plugin system.
|
314
|
+
|
315
|
+
In order to activate a plugin you must call the `plugin` method on the class:
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
class BaseOperation < Pathway::Operation
|
319
|
+
plugin :foobar, qux: 'quz'
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
class SomeOperation < BaseOperation
|
324
|
+
# The :foobar plugin will also be activated here
|
325
|
+
end
|
326
|
+
```
|
327
|
+
|
328
|
+
The plugin name must be specified as a `Symbol` (or also as the `Module` where is implemented, but more on that later), and can it take parameters next to the plugin's name.
|
329
|
+
When activated it will enrich your operations with new instance and class methods plus new customs step for the process DSL.
|
330
|
+
|
331
|
+
Mind you, if you wish to activate a plugin for a number of operations you can activate it for all of them directly on the `Pathway::Operation` class, or you can create your own base operation and all its descendants will inherit the base class' plugins.
|
332
|
+
|
121
333
|
#### `DryValidation` plugin
|
334
|
+
|
335
|
+
This plugin provides integration with the [dry-validation](http://dry-rb.org/gems/dry-validation/) gem. I won't explain in detail how to use this library since is already extensively documented on its official website, but instead I'll assume certain knowledge of it, nonetheless, as you'll see in a moment, its API pretty self-explanatory.
|
336
|
+
|
337
|
+
`dry-validation` provides a very simple way to define form (or schema) objects to process and validate our input. The provided custom `:validate` step allows you to run your input though a form to check your data is valid before continuing. When the input is invalid it will return an error object with type `:validation` and the reasons the validation failed on the `details` attribute. Is commonly the first step any operation runs.
|
338
|
+
|
339
|
+
When using this plugin we'll have to provide an already defined form to the step to use or we can also define one inline.
|
340
|
+
Let's see a few examples:
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
NuggetForm = Dry::Validation.Form do
|
344
|
+
required(:owner).filled(:str?)
|
345
|
+
required(:price).filled(:int?)
|
346
|
+
end
|
347
|
+
|
348
|
+
class CreateNugget < Pathway::Operation
|
349
|
+
plugin :dry_validation
|
350
|
+
|
351
|
+
form NuggetForm
|
352
|
+
|
353
|
+
process do
|
354
|
+
step :validate
|
355
|
+
step :create_nugget
|
356
|
+
end
|
357
|
+
|
358
|
+
# ...
|
359
|
+
end
|
360
|
+
```
|
361
|
+
|
362
|
+
As it can be seen at the code above, the form is first defined elsewhere, and the operation can be set up to use it by calling `form NuggetForm`, and use validate the input at the process block by calling `step :validate`.
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
class CreateNugget < Pathway::Operation
|
366
|
+
plugin :dry_validation
|
367
|
+
|
368
|
+
form do
|
369
|
+
required(:owner).filled(:str?)
|
370
|
+
required(:price).filled(:int?)
|
371
|
+
end
|
372
|
+
|
373
|
+
process do
|
374
|
+
step :validate
|
375
|
+
step :create_nugget
|
376
|
+
end
|
377
|
+
|
378
|
+
# ...
|
379
|
+
end
|
380
|
+
```
|
381
|
+
|
382
|
+
This second example is equivalent to the first one, but here we call `form` a block instead and no parameter; this block will be use as definition body for a form object that will be stored internally. This way you to keep the form and operation at the same place, which is convenient when you have a rather simpler form and don't need to reuse it.
|
383
|
+
|
384
|
+
One interesting nuance to keep in mind regarding the inline block form is that, when doing operation inheritance, if the parent operation already has a form, the child operation will define a new one extending from the parent's. This is very useful to share form functionality among related operations in the same class hierarchy.
|
385
|
+
|
386
|
+
##### Form options
|
387
|
+
|
388
|
+
If you are familiar with `dry-validation` you probably know it provides a way to [inject options](http://dry-rb.org/gems/dry-validation/basics/working-with-schemas/#injecting-external-dependencies) before calling the form instance.
|
389
|
+
|
390
|
+
On those scenarios you must either use the `auto_wire_options: true` plugin argument, or specify how to map options from the execution state to the form when calling `step :validate`.
|
391
|
+
Lets see and example for each case:
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
class CreateNugget < Pathway::Operation
|
395
|
+
plugin :dry_validation, auto_wire_options: true
|
396
|
+
|
397
|
+
context :user_name
|
398
|
+
|
399
|
+
form do
|
400
|
+
configure { options :user_name }
|
401
|
+
|
402
|
+
required(:owner).filled(:str?, :eql?: user_name)
|
403
|
+
required(:price).filled(:int?)
|
404
|
+
end
|
405
|
+
|
406
|
+
process do
|
407
|
+
step :validate
|
408
|
+
step :create_nugget
|
409
|
+
end
|
410
|
+
|
411
|
+
# ...
|
412
|
+
end
|
413
|
+
```
|
414
|
+
|
415
|
+
Here we see that the form needs a `:user_name` option so we tell the operation to grab the attribute with the same name from the execution state by activating `:auto_wire_options`, afterwards, when the validation runs, the form will already have the user name available.
|
416
|
+
|
417
|
+
Mind you, this option is `false` by default, so be sure to set it to `true` at `Pathway::Operation` if you'd rather have it for all your operations.
|
418
|
+
|
419
|
+
```ruby
|
420
|
+
class CreateNugget < Pathway::Operation
|
421
|
+
plugin :dry_validation
|
422
|
+
|
423
|
+
context :current_user_name
|
424
|
+
|
425
|
+
form do
|
426
|
+
configure { options :user_name }
|
427
|
+
|
428
|
+
required(:owner).filled(:str?, :eql?: user_name)
|
429
|
+
required(:price).filled(:int?)
|
430
|
+
end
|
431
|
+
|
432
|
+
process do
|
433
|
+
step :validate, with: { user_name: :current_user_name } # Inject :user_name to the form object using :current_user_name
|
434
|
+
step :create_nugget
|
435
|
+
end
|
436
|
+
|
437
|
+
# ...
|
438
|
+
end
|
439
|
+
```
|
440
|
+
|
441
|
+
On the other hand, if for some reason the name of the form's option and state attribute don't match, we can just pass `with: {...}` when calling to `step :validate`, indicating how to wire the attributes, the example above illustrates just that.
|
442
|
+
|
443
|
+
The `with:` parameter can always be specified, at `step :validate`, and allows you to override the default mapping regardless if auto-wiring is active or not.
|
444
|
+
|
445
|
+
#### `SimpleAuth` plugin
|
446
|
+
|
447
|
+
This very simple plugin adds a custom step called `:authorize`, that can be used to check for permissions and halt the operation with a `:forbidden` error when they aren't fulfilled.
|
448
|
+
|
449
|
+
In order to use it you must define a boolean predicate to check for permissions, by passing a block to the `authorization` method:
|
450
|
+
|
451
|
+
```ruby
|
452
|
+
class MyOperation < Pathway::Operation
|
453
|
+
plugin :simple_auth
|
454
|
+
|
455
|
+
context :current_user
|
456
|
+
authorization { current_user.is_admin? }
|
457
|
+
|
458
|
+
process do
|
459
|
+
step :authorize
|
460
|
+
step :perform_some_action
|
461
|
+
end
|
462
|
+
|
463
|
+
# ...
|
464
|
+
end
|
465
|
+
```
|
466
|
+
|
122
467
|
#### `SequelModels` plugin
|
468
|
+
|
469
|
+
The `sequel_models` plugin helps integrating operations with the [Sequel](http://sequel.jeremyevans.net/) ORM, by adding a few custom steps.
|
470
|
+
|
471
|
+
This plugin expects you to be using `Sequel` model classes to access your DB. In order to exploit it, you need to indicate which model your operation is going to work with, hence you must specify said model when activating the plugin with the `model:` keyword argument, or later using the `model` class method.
|
472
|
+
This configuration will then be used on the operation class and all its descendants.
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
class MyOperation < Pathway::Operation
|
476
|
+
plugin :sequel_models, model: Nugget, search_by: :name, set_result_key: false
|
477
|
+
end
|
478
|
+
|
479
|
+
# Or...
|
480
|
+
|
481
|
+
class MyOperation < Pathway::Operation
|
482
|
+
plugin :sequel_models
|
483
|
+
|
484
|
+
# This is useful when using inheritance and you need different models per operation
|
485
|
+
model Nugget, search_by: :name, set_result_key: false
|
486
|
+
|
487
|
+
process do
|
488
|
+
step :authorize
|
489
|
+
step :perform_some_action
|
490
|
+
end
|
491
|
+
end
|
492
|
+
```
|
493
|
+
|
494
|
+
As you can see above you can also customize the search field (`:search_by`) and indicate if you want to override the result key (`:set_result_key`) when calling to `model`.
|
495
|
+
These two options aren't mandatory, and by default pathway will set the search field to the class model primary key, and override the result key to a snake cased version of the model name (ignoring namespaces if contained inside a class or module).
|
496
|
+
|
497
|
+
Let's now take a look at the provided extensions:
|
498
|
+
|
499
|
+
##### `:fetch_model` step
|
500
|
+
|
501
|
+
This step will fetch a model from the DB, by extracting the search field from the `call` method input parameter stored at `:input` in the execution state. If the model cannot be fetched from the DB it will halt the execution with a `:not_found` error, otherwise it will simply save the model into the result key (which will be `:nugget` for the example below).
|
502
|
+
You can latter access the fetched model from that attribute and if the operation finish successfuly, it will be the operation result.
|
503
|
+
|
504
|
+
```ruby
|
505
|
+
class UpdateNugget < Pathway::Operation
|
506
|
+
plugin :sequel_models, model: Nugget
|
507
|
+
|
508
|
+
process do
|
509
|
+
step :validate
|
510
|
+
step :fetch_model
|
511
|
+
step :fetch_model, from: User, with: :user_id, search_by: :pk, to: :user # Even the default class can also be overrided with 'from:'
|
512
|
+
step :update_nugget
|
513
|
+
end
|
514
|
+
|
515
|
+
# ...
|
516
|
+
end
|
517
|
+
```
|
518
|
+
|
519
|
+
As a side note, and as you can see at the 3rd step, `:fetch_model` allows you to override the search column (`search_by:`), the input parameter to extract from `input` (`with:`), the attribute to store the result (`to:`) and even the default search class (`from:`). If the current defaults doesn't fit your needs and you'll have these options available. When, for instance, if you need some extra object to execute your operation.
|
520
|
+
|
521
|
+
##### `transaction` and `after_commit`
|
522
|
+
|
523
|
+
These two are bit special since they aren't actually custom steps but just new methods that extend the process DSL itself.
|
524
|
+
These methods will take a block as an argument within which you can define inner steps.
|
525
|
+
Keeping all that in mind the only thing `transaction` and `after_commit` really do is surround the inner steps with `SEQUEL_DB.transaction { ... }` and `SEQUEL_DB.after_commit { ... }` blocks, respectively.
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
class CreateNugget < Pathway::Operation
|
529
|
+
plugin :sequel_models, model: Nugget
|
530
|
+
|
531
|
+
process do
|
532
|
+
step :validate
|
533
|
+
transaction do
|
534
|
+
step :create_nugget
|
535
|
+
step :attach_history_note
|
536
|
+
after_commit do
|
537
|
+
step :send_emails
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# ...
|
543
|
+
end
|
544
|
+
```
|
545
|
+
|
546
|
+
When won't get into the details for each step in the example above, but the important thing to take away is that `:create_nugget` and `:attach_history_note` will exists withing a single transaction and `send_mails` (and any steps you add in the `after_commit` block) will only run after the transaction has finished successfuly.
|
547
|
+
|
548
|
+
Another nuance to take into account is that calling `transaction` will start a new savepoint, since, in case you're already inside a transaction, it will be able to properly notify that the transaction failed by returning an error object when that happens.
|
549
|
+
|
123
550
|
#### `Responder` plugin
|
124
551
|
|
552
|
+
This plugin extend the `call` class method on the operation in order to accept a block. You can then use this block for flow control on success and failure and to pattern match different type of errors.
|
553
|
+
|
554
|
+
There are two way to use this plugin: by discriminating between success and failure, and when by also discriminating according to the specific failure reason.
|
555
|
+
|
556
|
+
On each case you must provide the action to execute for every outcome using blocks:
|
557
|
+
|
558
|
+
```ruby
|
559
|
+
MyOperation.plugin :responder # 'plugin' is actually a public method
|
560
|
+
|
561
|
+
MyOperation.(context, params) do
|
562
|
+
success { |value| r.halt(200, value.to_json) } # BTW: 'r.halt' is a Roda request method used to exemplify
|
563
|
+
failure { |error| r.halt(403) }
|
564
|
+
end
|
565
|
+
```
|
566
|
+
|
567
|
+
On example above we provide a block for both the success and the failure case. On each block the result value or the error object error will be provided at the blocks' argument, the result of corresponding block will be the result of the whole expression.
|
568
|
+
|
569
|
+
Lets now show an example with pattern matching:
|
570
|
+
|
571
|
+
```ruby
|
572
|
+
MyOperation.plugin :responder
|
573
|
+
|
574
|
+
MyOperation.(context, params) do
|
575
|
+
success { |value| r.halt(200, value.to_json) }
|
576
|
+
failure(:forbidden) { |error| r.halt(403) }
|
577
|
+
failure(:validation) { |error| r.halt(422, error.details.to_json) }
|
578
|
+
failure(:not_found) { |error| r.halt(404) }
|
579
|
+
end
|
580
|
+
```
|
581
|
+
|
582
|
+
As you can see is almost identical as the previous example only that this time you provide the error type on each `failure` call.
|
583
|
+
|
584
|
+
### Plugin architecture
|
585
|
+
|
125
586
|
### Testing tools
|
126
587
|
#### Rspec config
|
127
588
|
#### Rspec matchers
|
128
589
|
|
129
|
-
## Best practices
|
130
|
-
### Operation object design and organization
|
131
|
-
### Testing recomendations
|
132
|
-
|
133
590
|
## Development
|
134
591
|
|
135
592
|
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.
|
data/lib/pathway/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pathway
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pablo Herrero
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-10-
|
11
|
+
date: 2017-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: inflecto
|
@@ -159,6 +159,7 @@ extra_rdoc_files: []
|
|
159
159
|
files:
|
160
160
|
- ".gitignore"
|
161
161
|
- ".rspec"
|
162
|
+
- CHANGELOG.md
|
162
163
|
- Gemfile
|
163
164
|
- LICENSE.txt
|
164
165
|
- README.md
|
@@ -169,10 +170,10 @@ files:
|
|
169
170
|
- bin/rspec
|
170
171
|
- bin/setup
|
171
172
|
- lib/pathway.rb
|
172
|
-
- lib/pathway/plugins/authorization.rb
|
173
173
|
- lib/pathway/plugins/dry_validation.rb
|
174
174
|
- lib/pathway/plugins/responder.rb
|
175
175
|
- lib/pathway/plugins/sequel_models.rb
|
176
|
+
- lib/pathway/plugins/simple_auth.rb
|
176
177
|
- lib/pathway/responder.rb
|
177
178
|
- lib/pathway/result.rb
|
178
179
|
- lib/pathway/rspec.rb
|