pathway 0.11.3 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f136383bd165f1ff88c16e5cb70fb16342b84e5b4e8b76bd49bf0fd91707e2ab
4
- data.tar.gz: b4b5cc55ea74d704c66976004e968f72d721c928af86bc8e5660ede8c8d61073
3
+ metadata.gz: 45170fed25a15c1d3b5b04630a9829a0a82d3a5f9423a3442445502252a1bb34
4
+ data.tar.gz: 424438af1eed0c2a93ca3be3dbe1923033f827809fa67d3b8aac31168402069e
5
5
  SHA512:
6
- metadata.gz: b83238e83c8f2835f5fa817b6c7e9bc91d68dd9362b97a371d2d22e5ff9a7f8695c26ac4a03fda5b23afebc4d6474744db85ad713c0f49eb2fec95b5696f206e
7
- data.tar.gz: b8fd39d4709179ecacd79c89741e6402610afad146117acc82eea7ba2f0376fefcfd5d990a848bd55c0acfdccd64b5313e2497893078818091d8c28b136dd8e2
6
+ metadata.gz: 1a2c4671a9482af68487fa0298bd81cb93940906969e4cf3ecdf170f2332d4b5b988eb77653e71b095619bb1a9e70473a3a94a0eca6462e1aee4cb0d11d8fc88
7
+ data.tar.gz: d6dfa471b5d28317b23ae7e671cb1a12052ba11c02a52cd639e4635505c3843c4b7bddf84a7afb3af616bcaae2a067b3f622432d6bf623902b7c7be56a3daa87
@@ -0,0 +1,29 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: [2.6, 2.7, 3.0, 3.1, 3.2, 3.3]
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.ruby-version }}
21
+ bundler: '2.4.22'
22
+ bundler-cache: true
23
+ - name: Run tests
24
+ run: bundle exec rake
25
+ - name: Coveralls GitHub Action
26
+ if: matrix.ruby-version == '3.2'
27
+ uses: coverallsapp/github-action@v2
28
+ with:
29
+ github-token: ${{ secrets.GITHUB_TOKEN }}
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.12.1] - 2024-06-23
2
+ ### Added
3
+ - Add support for pattern matching on `Result`, `State` and `Error` instances
4
+ - Add `Pathway::Result::Mixin` to allow easy constant lookup for `Result::Success` and `Result::Failure`
5
+
6
+ ## [0.12.0] - 2022-05-31
7
+ ### Changed
8
+ - Improve compatibility with Ruby 3.0
9
+ ### Added
10
+ - Add plugin `:auto_deconstruct_state` to help migrating old apps to Ruby 3.0
11
+
1
12
  ## [0.11.3] - 2020-07-22
2
13
  ### Changed
3
14
  - Use default error message on `:fetch_model` step, at `:sequel_models` plugin, if model type cannot be determined
@@ -11,7 +22,7 @@
11
22
  - Improve custom `rspec` matchers for testing field presence on schemas
12
23
 
13
24
  ## [0.11.0] - 2020-01-02
14
- ### Changed
25
+ ### Added
15
26
  - Add support for `dry-validation` 1.0 and above
16
27
 
17
28
  ## [0.10.0] - 2019-10-06
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Pathway
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/pathway.svg)](https://badge.fury.io/rb/pathway)
4
- [![CircleCI](https://circleci.com/gh/pabloh/pathway/tree/master.svg?style=shield)](https://circleci.com/gh/pabloh/pathway/tree/master)
4
+ [![Tests](https://github.com/pabloh/pathway/workflows/Tests/badge.svg)](https://github.com/oabloh/pathway/actions?query=workflow%3ATests)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/pabloh/pathway/badge.svg?branch=master)](https://coveralls.io/github/pabloh/pathway?branch=master)
6
6
 
7
7
  Pathway encapsulates your business logic into simple operation objects (AKA application services on the [DDD](https://en.wikipedia.org/wiki/Domain-driven_design) lingo).
@@ -12,22 +12,28 @@ Pathway encapsulates your business logic into simple operation objects (AKA appl
12
12
 
13
13
  ## Description
14
14
 
15
- Pathway helps you separate your business logic from the rest of your application; regardless if is an HTTP backend, a background processing daemon, etc.
16
- The main concept Pathway relies upon to build domain logic modules is the operation, this important concept will be explained in detail the following sections.
15
+ Pathway helps you separate your business logic from the rest of your application; regardless of is an HTTP backend, a background processing daemon, etc.
16
+ 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.
17
17
 
18
- 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.
18
+ 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 yield an organized and uniform codebase.
19
+
20
+ <!--
21
+ ## Migrating to Ruby 3.x
22
+
23
+ TODO: small comment and link to `auto_deconstruct_state` plugin
24
+ -->
19
25
 
20
26
  ## Usage
21
27
 
22
28
  ### Main concepts and API
23
29
 
24
- 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.
30
+ As mentioned earlier the operation is an essential concept Pathway is built around. Operations not only structure your code (using steps as will be explained later) but also express meaningful business actions. Operations can be thought of as use cases too: they represent an activity -to be performed by an actor interacting with the system- which should be understandable by anyone familiar with the business regardless of their technical expertise.
25
31
 
26
- 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.
32
+ Operations shouldn't ideally 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 data transformations required to make interactions with the inner system layers possible.
27
33
 
28
34
  #### Function object protocol (the `call` method)
29
35
 
30
- 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.
36
+ Operations work 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 need to follow.
31
37
  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.
32
38
 
33
39
  Let's see an example:
@@ -53,30 +59,28 @@ else
53
59
  end
54
60
  ```
55
61
 
56
- 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.
62
+ 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.
57
63
 
58
- Also, let's ignore the specifics about `Repository.create(...)`, we just need to know that is some backend service which can return a value.
64
+ Also, let's ignore the specifics about `Repository.create(...)`, we just need to know that is some backend service from which a value is returned.
59
65
 
60
66
 
61
- 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.
62
- 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.
67
+ We then define a `call` method for the class. It only checks if the result is available and then wraps it into a successful `Result` object when is ok, or a failing one when is not.
68
+ And basically, that's all is needed, you can then call the operation object, check whether it was completed correctly with `success?` and get the resulting value.
63
69
 
64
- 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.
70
+ 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 communicate 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.
65
71
 
66
72
  Maintaining always the same operation protocol will also be very useful when composing them.
67
73
 
68
-
69
74
  #### Operation result
70
75
 
71
- 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.
76
+ As should be evident by now an operation should always return either a successful or failed result. These concepts are represented by following a simple protocol, which `Pathway::Result` subclasses comply with.
72
77
 
73
- 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.
78
+ As we've seen before, by querying `success?` on the result we can see if the operation we just ran went well, or call to `failure?` to see if it failed.
74
79
 
75
80
  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.
76
- To return wrapped values or errors from your operation you must call to `Pathway::Result.success(value)` or `Pathway::Result.failure(error)`.
77
-
78
- 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:
81
+ To return wrapped values or errors from your operation you must call `Pathway::Result.success(value)` or `Pathway::Result.failure(error)`.
79
82
 
83
+ It is worth mentioning that when you inherit from `Pathway::Operation` you'll have helper methods at your disposal to create result objects easily. For instance, the previous section's example could be written as follows:
80
84
 
81
85
  ```ruby
82
86
  class MyFirstOperation < Pathway::Operation
@@ -90,8 +94,8 @@ end
90
94
 
91
95
  #### Error objects
92
96
 
93
- `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).
94
- Its 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 by the operation class:
97
+ `Pathway::Error` is a helper class to represent the error description from a failed operation execution (and also supports pattern matching as we'll see later).
98
+ Its use is completely optional but provides you with a basic schema to communicate what went wrong. You can instantiate it by calling `new` on the class itself or using the helper method `error` provided by the operation class:
95
99
 
96
100
  ```ruby
97
101
  class CreateNugget < Pathway::Operation
@@ -125,11 +129,11 @@ If you decide to use `Pathway::Error.new(...)` directly, you will have to pass a
125
129
 
126
130
  #### Initialization context
127
131
 
128
- 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.
132
+ 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 lifetime and won't make sense to pass as `call` parameters, you can provide these values on initialization as context data.
129
133
 
130
- 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.
134
+ Context data can be thought of as 'request data' on an HTTP endpoint, values that aren't global but won't change during the execution 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 these values on initialization, and probably pass them along to other operations down the line.
131
135
 
132
- 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:
136
+ You must define your initializer to accept a `Hash` with these 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:
133
137
 
134
138
  ```ruby
135
139
  class CreateNugget < Pathway::Operation
@@ -154,9 +158,9 @@ op = CreateNugget.new(current_user: user)
154
158
  op.call(foo: 'foobar')
155
159
  ```
156
160
 
157
- 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.
161
+ In 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.
158
162
 
159
- 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.
163
+ 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 them along easily.
160
164
 
161
165
  #### Alternative invocation syntax
162
166
 
@@ -168,24 +172,23 @@ context = { current_user: user }
168
172
 
169
173
  CreateNugget.call(context, params[:nugget]) # Using 'call' on the class
170
174
  ```
171
-
172
- 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.
175
+ Also, you have Ruby's alternative syntax to invoke the `call` method: `CreateNugget.(context, params[:nugget])`. In both cases, you'll get the operation result like when invoking `call` on the operation's instance.
173
176
 
174
177
  Mind you that a context must always be provided for this syntax, if you don't need any initialization use an empty hash.
175
178
 
176
- There's also third way to execute an operation, made available through a plugin, and will be explained later.
179
+ There's also a third way to execute an operation, made available through a plugin, that will be explained later.
177
180
 
178
181
  #### Steps
179
182
 
180
- 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`.
183
+ Finally, the steps are the heart of the `Operation` class and the main reason you will want to inherit your own classes from `Pathway::Operation`.
181
184
 
182
- 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.
185
+ 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.
183
186
 
184
- 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.
187
+ Every step should be cohesive and focused on a single responsibility, 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.
185
188
 
186
189
  ##### Process DSL
187
190
 
188
- Lets start by showing some actual code:
191
+ Let's start by showing some actual code:
189
192
 
190
193
  ```ruby
191
194
  # ...
@@ -199,44 +202,48 @@ Lets start by showing some actual code:
199
202
  # ...
200
203
  ```
201
204
 
202
- To define your `call` method using the DSL just call to `process` and pass a block, inside it the DSL will be available.
203
- 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 by `call`.
205
+ To define your `call` method using the DSL just call to `process` and pass a block, inside it, the DSL will be available.
206
+ 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 by `call`.
204
207
  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.
205
208
 
206
- 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 modify it, setting the result value or auxiliary keys to communicate with the next steps on the execution path.
209
+ 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 modify it, setting the result or auxiliary values, in order to communicate with the next steps on the execution path.
207
210
 
208
- 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.
211
+ Each step (as the operation as a whole) can succeed or fail, when the latter happens execution is halted, and the operation `call` method returns immediately.
209
212
  To signal a failure you must return a `failure(...)` or `error(...)` in the same fashion as when defining `call` directly.
210
213
 
211
- 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:`.
212
- Also non-failure return values inside steps are automatically wrapped so you can use `success` for clarity sake but it's optional.
214
+ 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, which will save your wrapped value, into the key provided at `to:`.
215
+ Also, non-failure return values inside steps are automatically wrapped so you can use `success` for clarity's sake but it's optional.
213
216
  If you omit the `to:` keyword argument when defining a `set` step, the result key will be used by default, but we'll explain more on that later.
214
217
 
215
218
  ##### Operation execution state
216
219
 
217
- 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 main methods (`:[]`, `:[]=`, `:fetch`, `:store`, `:include?` and `to_hash`).
220
+ 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 main methods (`:[]`, `:[]=`, `:fetch`, `:store`, `:include?` and `to_hash`).
218
221
 
219
222
  When an operation is executed, before running the first step, an initial state is created by copying all the values from the initialization context (and also including `input`).
220
- Note that these values can be replaced on later steps but it won't mutate the context object itself since is always frozen.
223
+ Note that these values can be replaced in later steps but it won't mutate the context object itself since is always frozen.
221
224
 
222
- A state object can be splatted on method definition in the same fashion as a `Hash`, thus, allowing to cherry pick the attributes we are interested on any given step:
225
+ A state object can be splatted on method definition in the same fashion as a `Hash`, thus, allowing us to cherry-pick the attributes we are interested in any given step:
223
226
 
224
227
  ```ruby
225
228
  # ...
226
229
  # This step only takes the values it needs and doesn't change the state.
227
- def send_emails(user:, report:, **)
230
+ def send_emails(state)
231
+ user, report = state[:user], state[:report]
228
232
  ReportMailer.send_report(user.email, report)
229
233
  end
230
234
  # ...
231
235
  ```
236
+ <!--
237
+ TODO: explain Ruby 2.7 and 3.0 state deconstruction alternatives
232
238
 
233
239
  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 the `**` when you have outstanding keys, Ruby's `Hash` destructing will fail.
240
+ -->
234
241
 
235
242
  ##### Successful operation result
236
243
 
237
- On each step you can access or change the operation result for a successful execution.
244
+ On each step, you can access or change the result the operation will produce on a successful execution.
238
245
  The value will be stored at one of the attributes within the state.
239
- By default the state's key `:value` will hold the result, but if you prefer to use another name you can specify it through the `result_at` operation class method.
246
+ By default, the state's key `:value` will hold the result, but if you prefer to use another name you can specify it through the `result_at` operation class method.
240
247
 
241
248
  ##### Full example
242
249
 
@@ -271,36 +278,36 @@ class CreateNugget < Pathway::Operation
271
278
  end
272
279
  end
273
280
 
274
- def create_nugget(:params, **)
275
- Nugget.create(owner: current_user, **params)
276
- def
281
+ def create_nugget(state)
282
+ Nugget.create(owner: current_user, **state[:params])
283
+ end
277
284
 
278
- def notify(:nugget, **)
279
- Notifier.notify(:new_nugget, nugget)
280
- else
285
+ def notify(state)
286
+ Notifier.notify(:new_nugget, state[:nugget])
287
+ end
281
288
  end
282
289
  ```
283
290
 
284
- The example above the operation will produce a nugget (whatever that is...).
291
+ In the example above the operation will produce a nugget (whatever that is...).
285
292
 
286
- As you can see in the code, we are using the previously mentioned methods to indicate we need a current user to be present on initialization: `context: current_user`, a `call` method (defined by `process do ... end`), and that the result value should be stored at the `:nugget` key (`result_at :nugget`).
293
+ As you can see in the code, we are using the previously mentioned methods to indicate we need the current user to be present on initialization: `context: current_user`, a `call` method (defined by `process do ... end`), and the result value should be stored at the `:nugget` key (`result_at :nugget`).
287
294
 
288
- 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.
295
+ Let's 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.
289
296
 
290
297
  Now, for each of the step methods:
291
298
 
292
- - `:authorize` doesn't need 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.
299
+ - `:authorize` doesn't need the state so just ignores it, then checks if the current user is allowed to run the operation and halts the execution by returning a `:forbidden` error type if is not, otherwise does nothing and the execution goes on.
293
300
  - `: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 on success the return value is `state[:params]`, but is ignored like on `:authorize`, since this method was also specified using `step`.
294
- - `: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:`.
301
+ - `:create_nugget` first takes the `:params` attribute from the state, 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:`.
295
302
  - `:notify` grabs the `:nugget` from the state, and simply emits a notification with it, it has no meaningful return value, so is ignored.
296
303
 
297
- This example basically touches all the essential concepts needed for defining an operation class. If you can grasp it you've already a good understanding on how to implement one. There are still some very important bits to cover (like testing), and we'll tackle them on later sections.
304
+ The previous example goes through 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 them in the latter sections.
298
305
 
299
- On a final note, you may be thinking 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 name?, it could be possible infer it from the surrounding code. We will address all those issues on the next section using plugins, `pathway`'s extension mechanism.
306
+ On a final note, you may be thinking the code could be a bit less verbose; also, shouldn't very common stuff like validation or authorization be simpler to use? and why always specify the result key name? maybe is possible to infer it from the surrounding code. We will address all those issues in the next section using plugins, `pathway`'s extension mechanism.
300
307
 
301
308
  ### Plugins
302
309
 
303
- Pathway operations can be extended with plugins. They are very similar as the ones 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 with `pathway`'s plugin system.
310
+ Pathway operations can be extended with plugins. They are very similar to the ones 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 with `pathway`'s plugin system.
304
311
 
305
312
  To activate a plugin just call the `plugin` method on the operation class:
306
313
 
@@ -321,9 +328,9 @@ Mind you, if you wish to activate a plugin for a number of operations you can ac
321
328
 
322
329
  #### `DryValidation` plugin
323
330
 
324
- 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.
331
+ 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 is pretty self-explanatory.
325
332
 
326
- `dry-validation` provides a very simple way to define contract objects (conceptually very similar to form objects) to process and validate input. The provided custom `:validate` step allows you to run your input though a contract to check if your data is valid before carrying on. When the input is invalid it will return an error object of type `:validation` and the reasons the validation failed will be available at the `details` attribute. Is usually the first step an operation runs.
333
+ `dry-validation` provides a very simple way to define contract objects (conceptually very similar to form objects) to process and validate input. The provided custom `:validate` step allows you to run your input through a contract to check if your data is valid before carrying on. When the input is invalid it will return an error object of type `:validation` and the reasons the validation failed will be available at the `details` attribute. Is usually the first step an operation runs.
327
334
 
328
335
  When using this plugin we can provide an already defined contract to the step to use or we can also define it within the operation.
329
336
  Let's see a few examples:
@@ -350,7 +357,7 @@ class CreateNugget < Pathway::Operation
350
357
  end
351
358
  ```
352
359
 
353
- As is is shown above, the contract is defined first, then is configured it will be used by the operation by calling `contract NuggetContract`, and validate the input at the process block by placing the step `step :validate` inside the `process` block.
360
+ As is shown above, the contract is defined first, then configured to be used by the operation by calling `contract NuggetContract`, and validate the input at the process block by placing the step `step :validate` inside the `process` block.
354
361
 
355
362
  ```ruby
356
363
  class CreateNugget < Pathway::Operation
@@ -372,11 +379,11 @@ class CreateNugget < Pathway::Operation
372
379
  end
373
380
  ```
374
381
 
375
- Now, this second example is equivalent to the first one, but here we call `contract` with a block instead and no parameter; this block will be used as definition body for a contract class that will be stored internally. Thus keeping the contract and operation code at the same place, this is convenient when you have a rather simpler contract and don't need to reuse it.
382
+ Now, this second example is equivalent to the first one, but here we call `contract` with a block instead of an object parameter; this block will be used as the definition body for a contract class that will be stored internally. Thus keeping the contract and operation code in the same place, this is convenient when you have a rather simpler contract and don't need to reuse it.
376
383
 
377
384
  One interesting nuance to keep in mind regarding the inline block contract is that, when doing operation inheritance, if the parent operation already has a contract, the child operation will define a new one inheriting from the parent's. This is very useful to share validation logic among related operations in the same class hierarchy.
378
385
 
379
- As a side note, if your contract is simple enough and only have params, you can call the `params` method directly instead, the following code is essentially equivalent to previous example:
386
+ As a side note, if your contract is simple enough and has parameters and not extra validations rules, you can call the `params` method directly instead, the following code is essentially equivalent to the previous example:
380
387
 
381
388
  ```ruby
382
389
  class CreateNugget < Pathway::Operation
@@ -400,7 +407,7 @@ end
400
407
 
401
408
  If you are familiar with `dry-validation` you probably know it provides a way to [inject options](https://dry-rb.org/gems/dry-validation/1.4/external-dependencies/) before calling the contract.
402
409
 
403
- 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 contract when calling `step :validate`.
410
+ In those scenarios, you must either set the `auto_wire_options: true` plugin argument or specify how to map options from the execution state to the contract when calling `step :validate`.
404
411
  Lets see and example for the first case:
405
412
 
406
413
  ```ruby
@@ -465,11 +472,11 @@ class CreateNugget < Pathway::Operation
465
472
  end
466
473
  ```
467
474
 
468
- 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.
475
+ The `with:` parameter can always be specified for `step :validate`, and allows you to override the default mapping regardless if auto-wiring is active or not.
469
476
 
470
477
  ##### Older versions of `dry-validation`
471
478
 
472
- Pathway supports the `dry-validation` gem down to version `0.11` (inclusive) in case you still have unmigrated code. When using versions bellow `1.0` the concept of contract is not present and instead of calling the `contract` method to setup your validation logic you must use the `form` method. Everything else remains the same except, obviously, that you would have to use `dry-definition`'s [old API](https://dry-rb.org/gems/dry-validation/0.13/) which is a bit different from the current one.
479
+ Pathway supports the `dry-validation` gem down to version `0.11` (inclusive) in case you still have unmigrated code. When using versions below `1.0` the concept of contract is not present and instead of calling the `contract` method to set up your validation logic, you must use the `form` method. Everything else remains the same except, obviously, that you would have to use `dry-definition`'s [old API](https://dry-rb.org/gems/dry-validation/0.13/) which is a bit different from the current one.
473
480
 
474
481
  #### `SimpleAuth` plugin
475
482
 
@@ -495,7 +502,7 @@ end
495
502
 
496
503
  #### `SequelModels` plugin
497
504
 
498
- The `sequel_models` plugin helps integrating operations with the [Sequel](http://sequel.jeremyevans.net/) ORM, by adding a few custom steps.
505
+ The `sequel_models` plugin helps integrate operations with the [Sequel](http://sequel.jeremyevans.net/) ORM, by adding a few custom steps.
499
506
 
500
507
  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.
501
508
  This configuration will then be used on the operation class and all its descendants.
@@ -520,15 +527,15 @@ class MyOperation < Pathway::Operation
520
527
  end
521
528
  ```
522
529
 
523
- 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`.
524
- 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).
530
+ As you can see above you can also customize the search field (`:search_by`) and indicate if you want to override or not the result key (`:set_result_key`) when calling the `model` method.
531
+ 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).
525
532
 
526
533
  Let's now take a look at the provided extensions:
527
534
 
528
535
  ##### `:fetch_model` step
529
536
 
530
537
  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).
531
- You can latter access the fetched model from that attribute and if the operation finish successfuly, it will be the operation result.
538
+ You can later access the fetched model from that attribute and if the operation finishes successfully, it will be used as its result.
532
539
 
533
540
  ```ruby
534
541
  class UpdateNugget < Pathway::Operation
@@ -545,11 +552,11 @@ class UpdateNugget < Pathway::Operation
545
552
  end
546
553
  ```
547
554
 
548
- As a side note, and as shown on the 3rd step, `:fetch_model` allows you to override the search column (`search_by:`), the input parameter to extract from `input` (`using:`), 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. This is commonly usefuly when you need some extra object, besides the main one, to execute your operation.
555
+ As a side note, and as shown in the 3rd step, `:fetch_model` allows you to override the search column (`search_by:`), the input parameter to extract from `input` (`using:`), the attribute to store the result (`to:`) and even the default search class (`from:`). If the current defaults don't fit your needs and you'll have these options available. This is commonly useful when you need some extra object, besides the main one, to execute your operation.
549
556
 
550
557
  ##### `transaction` and `after_commit`
551
558
 
552
- These two are bit special since they aren't actually custom steps but just new methods that extend the process DSL itself.
559
+ These two are a bit special since they aren't actually custom steps but just new methods that extend the process DSL itself.
553
560
  These methods will take a block as an argument within which you can define inner steps.
554
561
  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.
555
562
 
@@ -572,17 +579,17 @@ class CreateNugget < Pathway::Operation
572
579
  end
573
580
  ```
574
581
 
575
- 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.
582
+ 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 within a single transaction and `send_mails` (and any steps you add in the `after_commit` block) will only run after the transaction has finished successfully.
576
583
 
577
584
  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.
578
585
 
579
586
  #### `Responder` plugin
580
587
 
581
- 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.
588
+ This plugin extends the `call` class method on the operation to accept a block. You can then use this block for flow control on success, failure, and also different types of failures.
582
589
 
583
- 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.
590
+ There are two ways to use this plugin: by discriminating between success and failure, and also discriminating according to the specific failure type.
584
591
 
585
- On each case you must provide the action to execute for every outcome using blocks:
592
+ In each case you must provide the action to execute for every outcome using blocks:
586
593
 
587
594
  ```ruby
588
595
  MyOperation.plugin :responder # 'plugin' is actually a public method
@@ -592,10 +599,18 @@ MyOperation.(context, params) do
592
599
  failure { |error| r.halt(403) }
593
600
  end
594
601
  ```
602
+ <!--
603
+ ```ruby
604
+ case MyOperation.(context, params)
605
+ in Success(value) then r.halt(200, value.to_json)
606
+ in Failure(_) then r.halt(403)
607
+ end
608
+ ```
609
+ -->
595
610
 
596
- 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.
611
+ In the 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, and the result of the corresponding block will be the result of the whole expression.
597
612
 
598
- Lets now show an example with pattern matching:
613
+ Lets now show an example with the error type specified:
599
614
 
600
615
  ```ruby
601
616
  MyOperation.plugin :responder
@@ -607,19 +622,34 @@ MyOperation.(context, params) do
607
622
  failure(:not_found) { |error| r.halt(404) }
608
623
  end
609
624
  ```
625
+ <!--
626
+ ```ruby
627
+ case MyOperation.(context, params)
628
+ in Success(value) then r.halt(200, value.to_json)
629
+ in Failure(type: :forbidden) then r.halt(403)
630
+ in failure(type: :validation, details:) then r.halt(422, details.to_json)
631
+ in failure(type: :not_found) then r.halt(404)
632
+ end
633
+ ```
634
+ -->
635
+
636
+ As you can see is almost identical to the previous example only that this time you provide the error type on each `failure` call.
610
637
 
611
- As you can see is almost identical as the previous example only that this time you provide the error type on each `failure` call.
638
+ <!--
639
+ #### `AutoDeconstructState` plugin
612
640
 
641
+ TODO: Explain reason, how to migrate, how to activate
642
+ -->
613
643
  ### Plugin architecture
614
644
 
615
- Going a bit deeper now, we'll explain how to implement your own plugins. As was mention before `pathway` follows a very similar approach to the [Roda](http://roda.jeremyevans.net/) or [Sequel](http://sequel.jeremyevans.net/) plugin systems, which is reflected on its implementation.
645
+ Going a bit deeper now, we'll explain how to implement your own plugins. As was mentioned before `pathway` follows a very similar approach to the [Roda](http://roda.jeremyevans.net/) or [Sequel](http://sequel.jeremyevans.net/) plugin systems, which is reflected at its implementation.
616
646
 
617
- Each plugin must be defined in a file placed within the `pathway/plugins/` directory of your gem or application, so `pathway` can require the file; and must be implemented as a module inside the `Pathway::Plugins` namespace module. Inside your plugin module, three extra modules can be define to extend the operation API `ClassMethods`, `InstanceMethods` and `DSLMethods`; plus a class method `apply` for plugin initialization when needed.
647
+ Each plugin must be defined in a file placed within the `pathway/plugins/` directory of your gem or application, so `pathway` can require the file; and must be implemented as a module inside the `Pathway::Plugins` namespace module. Inside your plugin module, three extra modules can be defined to extend the operation API `ClassMethods`, `InstanceMethods` and `DSLMethods`; plus a class method `apply` for plugin initialization when needed.
618
648
 
619
- If you are familiar with the aforementioned plugin mechanism (or other as well), the function of each module is probably starting to feel evident: `ClassMethods` will be used to extend the operation class, so any class methods should be defined here; `InstanceMethods` will be included on the operation so all the instance methods you need to add to the operation should be here, this include every custom step you need to add; and finally `DSLMethods` will be included on the `Operation::DSL` class, which holds all the DSL methods like `step` or `set`.
649
+ If you are familiar with the aforementioned plugin mechanism (or others as well), the function of each module is probably starting to feel evident: `ClassMethods` will be used to extend the operation class, so any class methods should be defined here; `InstanceMethods` will be included on the operation so all the instance methods you need to add to the operation should be here, this includes every custom step you need to add; and finally `DSLMethods` will be included on the `Operation::DSL` class, which holds all the DSL methods like `step` or `set`.
620
650
  The `apply` method will simply be run whenever the plugin is included, taking the operation class on the first argument and all then arguments the call to `plugin` received (excluding the plugin name).
621
651
 
622
- Lets explain with more detail using a complete example:
652
+ Let's explain with more detail using a complete example:
623
653
 
624
654
  ```ruby
625
655
  # lib/pathway/plugins/active_record.rb
@@ -678,31 +708,31 @@ end
678
708
  The code above implements a plugin to provide basic interaction with the [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html) gem.
679
709
  Even though is a very simple plugin, it shows all the essentials to develop more complex ones.
680
710
 
681
- As is pointed out in the code, some of the methods implemented here (`fetch_model` and `transmission`) collide with methods defined for `:sequel_models`, so as a consequence, these two plugin's are not compatible with each other and cannot be activated for the same operation (although you can still do it for different operations within the same application).
682
- You must be mindful about colliding method names when mixing plugins, since `Pathway` can't bookkeep compatibility among every plugin that exists of will ever exist.
711
+ As is pointed out in the code, some of the methods implemented here (`fetch_model` and `transmission`) collide with methods defined for `:sequel_models`, so as a consequence, these two plugins are not compatible with each other and cannot be activated for the same operation (although you can still do it for different operations within the same application).
712
+ You must be mindful about colliding method names when mixing plugins since `Pathway` can't bookkeep compatibility among every plugin that exists or will ever exist.
683
713
  Is a good practice to document known incompatibilities on the plugin definition itself when they are known.
684
714
 
685
- The whole plugin is completely defined within the `ActiveRecord` module inside the `Pathway::Plugins` namespace, also the file is placed at the load path in `pathway/plugin/active_record.rb` (assuming `lib/` is listed in `$LOAD_PATH`). This will ensure, when calling `plugin :active_record` inside an operation, the correct file will be loaded and the correct plugin module will be applied to the current operation.
715
+ The whole plugin is completely defined within the `ActiveRecord` module inside the `Pathway::Plugins` namespace, also the file is placed at the load path in `pathway/plugin/active_record.rb` (assuming `lib/` is listed in `$LOAD_PATH`). This will ensure when calling `plugin :active_record` inside an operation, the correct file will be loaded and the correct plugin module will be applied to the current operation.
686
716
 
687
717
  Moving on to the `ClassMethods` module, we can see the accessors `model` and `pk` are defined for the operation's class to allow configuration.
688
- Also, the `inherited` hook is defined, this will simply be another class method at the operation and as such will be executed normally when the operation class is inherited. In our implementation we just call to `super` (which is extremely important since other modules or parent classes could be using this hook), and then copy the `model` and `pk` options from the parent to the subclass in order to propagate the configuration downwards.
718
+ Also, the `inherited` hook is defined, this will simply be another class method at the operation and as such will be executed normally when the operation class is inherited. In our implementation, we just call to `super` (which is extremely important since other modules or parent classes could be using this hook) and then copy the `model` and `pk` options from the parent to the subclass in order to propagate the configuration downwards.
689
719
 
690
- At the end of the `ActiveRecord` module definition you can see the `apply` method. It will receive the operation class and the parameters passed when the `plugin` method is invoked. This method is usually used for loading dependencies of just setting up config parameters as we do in this particular example.
720
+ At the end of the `ActiveRecord` module definition, you can see the `apply` method. It will receive the operation class and the parameters passed when the `plugin` method is invoked. This method is usually used for loading dependencies or just setting up config parameters as we do in this particular example.
691
721
 
692
- `InstanceMethods` first defines a few delegator methods to the class itself to use later.
693
- Then the `fetch_model` step is defined (remember steps are but operation instance methods). Its first parameter is the state itself, as in the other steps we've seen before, and the remaining parameters are the options we can pass when calling `step :fetch_model` (mind you, this is also valid for steps defined in operations classes). Here we only take a single keyword argument: `column: pk`, with a default value; this will allow us to change the look up column when using the step, and is the only parameter we can use, passing other keyword arguments or extra positional parameters when invoking the step will raise errors.
722
+ `InstanceMethods` first defines a few delegator methods to the class itself for later use.
723
+ Then the `fetch_model` step is defined (remember steps are but operation instance methods). Its first parameter is the state itself, as in the other steps we've seen before, and the remaining parameters are the options we can pass when calling `step :fetch_model` (mind you, this is also valid for steps defined in operations classes). Here we only take a single keyword argument: `column: pk`, with a default value; this will allow us to change the look-up column when using the step and is the only parameter we can use, passing other keyword arguments or extra positional parameters when invoking the step will raise errors.
694
724
 
695
- Let's now examine the `fetch_model` step body, is not really that much different from other steps, here we extract the model primary key from `state[:input][column]` and use it to perform a search. If nothing is found an error is returned, otherwise the state is updated on the result key to hold the model we just fetched from the DB.
725
+ Let's now examine the `fetch_model` step body, it's not really that much different from other steps, here we extract the model primary key from `state[:input][column]` and use it to perform a search. If nothing is found an error is returned, otherwise the state is updated in the result key, to hold the model that was just fetched from the DB.
696
726
 
697
727
  We finally see a `DSLMethods` module defined to extend the process DSL.
698
- For this plugin we'll define a way to group steps within an `ActiveRecord` transaction, much in the same way the `:sequel_models` plugin already does for `Sequel`.
699
- To this end we define a `transaction` method to expect a steps block and pass it down to the `around` helper below which expects a callable (like a `Proc`) and a step list block. As you can see the lambda we pass on the first parameter is the one that makes sure the steps are being run inside a transaction and to abort the transaction if the intermediate result is a failure.
728
+ For this plugin, we'll define a way to group steps within an `ActiveRecord` transaction, much in the same way the `:sequel_models` plugin already does for `Sequel`.
729
+ To this end, we define a `transaction` method to expect a steps block and pass it down to the `around` helper below which expects a callable (like a `Proc`) and a step list block. As you can see the lambda we pass on the first parameter makes sure the steps are being run inside a transaction or aborts the transaction if the intermediate result is a failure.
700
730
 
701
- The `around` method is a low level tool available to help extending the process DSL and it may seem a bit daunting at first glance but it usage is quite simple, the block is just a step list like the ones we find inside the `process` call; and the parameter is a callable (usually a lambda), that will take 2 arguments, an object from which we can run the step list by invoking `call` (and is the only thing it can do), and the current state. From here we can examine the state and decide upon whether to run the steps, how many times (if any) or run some code before and/or after doing so, like what we need to do in our example to surround the steps within a DB transaction.
731
+ The `around` method is a low-level tool available to help extend the process DSL and it may seem a bit daunting at first glance but its usage is quite simple, the block is just a step list like the ones we find inside the `process` call; and the parameter is a callable (usually a lambda), that will take 2 arguments, an object from which we can run the step list by invoking `call` (and is the only thing it can do), and the current state. From here we can examine the state and decide upon whether to run the steps, how many times (if any), or run some code before and/or after doing so, like what we need to do in our example to surround the steps within a DB transaction.
702
732
 
703
733
  ### Testing tools
704
734
 
705
- As of right now only `rspec` is supported, that is, you can obviously test your operations with any framework you want, but all the provided matchers are designed for `rspec`.
735
+ As of right now, only `rspec` is supported, that is, you can obviously test your operations with any framework you want, but all the provided matchers are designed for `rspec`.
706
736
 
707
737
  #### Rspec config
708
738
 
@@ -714,8 +744,8 @@ require 'pathway/rspec'
714
744
 
715
745
  #### Rspec matchers
716
746
 
717
- Pathway provides a few matchers in order to tests your operation easier.
718
- Let's go through a full example and break it up in the following subsections:
747
+ Pathway provides a few matchers in order to test your operation easier.
748
+ Let's go through a full example:
719
749
 
720
750
  ```ruby
721
751
  # create_nugget.rb
@@ -734,8 +764,8 @@ class CreateNugget < Pathway::Operation
734
764
  set :create_nugget
735
765
  end
736
766
 
737
- def create_nugget(params:,**)
738
- Nugget.create(params)
767
+ def create_nugget(state)
768
+ Nugget.create(state[:params])
739
769
  end
740
770
  end
741
771
 
@@ -774,7 +804,7 @@ end
774
804
  ##### `succeed_on` matcher
775
805
 
776
806
  This first matcher works on the operation itself and that's why we could set `subject` with the operation instance and use `is_expected.to succeed_on(...)` on the example.
777
- The assertion it performs is simply is that the operation was successful, also you can optionally chain `returning(...)` if you want to test the returning value, this method allows nesting matchers as is the case in the example.
807
+ The assertion it performs is simply that the operation was successful, also you can optionally chain `returning(...)` if you want to test the returning value, this method allows nesting matchers as is the case in the example.
778
808
 
779
809
  ##### `fail_on` matcher
780
810
 
@@ -782,7 +812,7 @@ This second matcher is analog to `succeed_on` but it asserts that operation exec
782
812
 
783
813
  ##### contract/form matchers
784
814
 
785
- Finally we can see that we are also testing the operation's contract (or form), implemented here with the `dry-validation` gem.
815
+ Finally, we can see that we are also testing the operation's contract (or form), implemented here with the `dry-validation` gem.
786
816
 
787
817
  Two more matchers are provided: `require_fields` (aliased `require_field`) to test when a contract is expected to define a required set of fields, and `accept_optional_fields` (aliased `accept_optional_field`) to test when a contract must define a certain set of optional fields, both the contract class (at operation class method `contract_class`) or an instance (operation class method `build_contract`) can be provided.
788
818
 
data/Rakefile CHANGED
@@ -3,6 +3,10 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ unless RUBY_VERSION =~ /^2\.7|^3\./
8
+ t.exclude_pattern = 'spec/operation_call_pattern_matching_spec.rb,spec/state_pattern_matching_spec.rb'
9
+ end
10
+ end
7
11
 
8
12
  task :default => :spec
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pathway
4
+ module Plugins
5
+ module AutoDeconstructState
6
+ module DSLMethods
7
+ private
8
+
9
+ def _callable(callable)
10
+ if callable.is_a?(Symbol) && @operation.respond_to?(callable, true) &&
11
+ @operation.method(callable).parameters.all? { _1 in [:key|:keyreq|:keyrest|:block,*] }
12
+
13
+ -> state { @operation.send(callable, **state) }
14
+ else
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ if RUBY_VERSION =~ /^3\./
4
+ require 'pathway/plugins/auto_deconstruct_state/ruby3'
5
+ end
6
+
7
+ module Pathway
8
+ module Plugins
9
+ module AutoDeconstructState
10
+ end
11
+ end
12
+ end
@@ -19,7 +19,7 @@ module Pathway
19
19
  end
20
20
  end
21
21
 
22
- def params(*args, &block)
22
+ ruby2_keywords def params(*args, &block)
23
23
  contract { params(*args, &block) }
24
24
  end
25
25
 
@@ -29,8 +29,8 @@ module Pathway
29
29
  @builded_contract = @contract_options.empty? && klass.schema ? klass.new : nil
30
30
  end
31
31
 
32
- def build_contract(opts = {})
33
- @builded_contract || contract_class.new(opts)
32
+ def build_contract(**opts)
33
+ @builded_contract || contract_class.new(**opts)
34
34
  end
35
35
 
36
36
  def inherited(subclass)
@@ -57,12 +57,12 @@ module Pathway
57
57
  with ||= contract_options.zip(contract_options).to_h
58
58
  end
59
59
  opts = Hash(with).map { |to, from| [to, state[from]] }.to_h
60
- validate_with(state[:input], opts)
60
+ validate_with(state[:input], **opts)
61
61
  .then { |params| state.update(params: params) }
62
62
  end
63
63
 
64
- def validate_with(input, opts = {})
65
- result = contract(opts).call(input)
64
+ def validate_with(input, **opts)
65
+ result = contract(**opts).call(input)
66
66
 
67
67
  result.success? ? wrap(result.values.to_h) : error(:validation, details: result.errors.to_h)
68
68
  end
@@ -4,8 +4,8 @@ module Pathway
4
4
  module Plugins
5
5
  module Responder
6
6
  module ClassMethods
7
- def call(ctx, *params, &bl)
8
- result = super(ctx, *params)
7
+ ruby2_keywords def call(*args, &bl)
8
+ result = super(*args)
9
9
  block_given? ? Responder.respond(result, &bl) : result
10
10
  end
11
11
  end
@@ -84,8 +84,8 @@ module Pathway
84
84
  end
85
85
  end
86
86
 
87
- def self.apply(operation, model: nil, **args)
88
- operation.model(model, args) if model
87
+ def self.apply(operation, model: nil, **kwargs)
88
+ operation.model(model, **kwargs) if model
89
89
  end
90
90
  end
91
91
  end
@@ -22,6 +22,10 @@ module Pathway
22
22
  follow = self.then(bl, &block)
23
23
  follow.failure? ? follow : self
24
24
  end
25
+
26
+ private
27
+
28
+ alias :value_for_deconstruct :value
25
29
  end
26
30
 
27
31
  class Failure < Result
@@ -40,6 +44,27 @@ module Pathway
40
44
  def tee(_=nil)
41
45
  self
42
46
  end
47
+
48
+ private
49
+
50
+ alias :value_for_deconstruct :error
51
+ end
52
+
53
+ module Mixin
54
+ Success = Result::Success
55
+ Failure = Result::Failure
56
+ end
57
+
58
+ def deconstruct
59
+ [value_for_deconstruct]
60
+ end
61
+
62
+ def deconstruct_keys(keys)
63
+ if value_for_deconstruct.respond_to?(:deconstruct_keys)
64
+ value_for_deconstruct.deconstruct_keys(keys)
65
+ else
66
+ {}
67
+ end
43
68
  end
44
69
 
45
70
  def failure?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pathway
4
- VERSION = '0.11.3'
4
+ VERSION = '0.12.1'
5
5
  end
data/lib/pathway.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ruby2_keywords'
3
4
  require 'forwardable'
4
5
  require 'dry/inflector'
5
6
  require 'contextualizer'
@@ -9,21 +10,23 @@ require 'pathway/result'
9
10
  module Pathway
10
11
  Inflector = Dry::Inflector.new
11
12
  class Operation
12
- def self.plugin(name, *args)
13
- require "pathway/plugins/#{Inflector.underscore(name)}" if name.is_a?(Symbol)
13
+ class << self
14
+ ruby2_keywords def plugin(name, *args)
15
+ require "pathway/plugins/#{Inflector.underscore(name)}" if name.is_a?(Symbol)
14
16
 
15
- plugin = name.is_a?(Module) ? name : Plugins.const_get(Inflector.camelize(name))
17
+ plugin = name.is_a?(Module) ? name : Plugins.const_get(Inflector.camelize(name))
16
18
 
17
- self.extend plugin::ClassMethods if plugin.const_defined? :ClassMethods
18
- self.include plugin::InstanceMethods if plugin.const_defined? :InstanceMethods
19
- self::DSL.include plugin::DSLMethods if plugin.const_defined? :DSLMethods
19
+ self.extend plugin::ClassMethods if plugin.const_defined? :ClassMethods
20
+ self.include plugin::InstanceMethods if plugin.const_defined? :InstanceMethods
21
+ self::DSL.include plugin::DSLMethods if plugin.const_defined? :DSLMethods
20
22
 
21
- plugin.apply(self, *args) if plugin.respond_to?(:apply)
22
- end
23
+ plugin.apply(self, *args) if plugin.respond_to?(:apply)
24
+ end
23
25
 
24
- def self.inherited(subclass)
25
- super
26
- subclass.const_set :DSL, Class.new(self::DSL)
26
+ def inherited(subclass)
27
+ super
28
+ subclass.const_set :DSL, Class.new(self::DSL)
29
+ end
27
30
  end
28
31
 
29
32
  class DSL
@@ -42,6 +45,14 @@ module Pathway
42
45
  @details = details || {}
43
46
  end
44
47
 
48
+ def deconstruct
49
+ [type, message, details]
50
+ end
51
+
52
+ def deconstruct_keys(_)
53
+ { type: type, message: message, details: details }
54
+ end
55
+
45
56
  private
46
57
 
47
58
  def default_message_for(type)
@@ -57,7 +68,7 @@ module Pathway
57
68
  @result_key = operation.result_key
58
69
  end
59
70
 
60
- delegate %i([] []= fetch store include? values_at) => :@hash
71
+ delegate %i([] []= fetch store include? values_at deconstruct_keys) => :@hash
61
72
 
62
73
  def update(kargs)
63
74
  @hash.update(kargs)
@@ -90,7 +101,7 @@ module Pathway
90
101
  end
91
102
  end
92
103
 
93
- def call(ctx, *params)
104
+ ruby2_keywords def call(ctx, *params)
94
105
  new(ctx).call(*params)
95
106
  end
96
107
 
@@ -113,7 +124,7 @@ module Pathway
113
124
  end
114
125
 
115
126
  def error(type, message: nil, details: nil)
116
- failure Error.new(type: type, message: message, details: details)
127
+ failure(Error.new(type: type, message: message, details: details))
117
128
  end
118
129
 
119
130
  def wrap_if_present(value, type: :not_found, message: nil, details: {})
@@ -137,7 +148,7 @@ module Pathway
137
148
  end
138
149
 
139
150
  # Execute step and preserve the former state
140
- def step(callable, *args)
151
+ ruby2_keywords def step(callable, *args)
141
152
  bl = _callable(callable)
142
153
 
143
154
  @result = @result.tee { |state| bl.call(state, *args) }
@@ -190,9 +201,9 @@ module Pathway
190
201
  def _callable(callable)
191
202
  case callable
192
203
  when Proc
193
- -> *args { @operation.instance_exec(*args, &callable) }
204
+ -> *args { @operation.instance_exec(*args, &callable) }.ruby2_keywords
194
205
  when Symbol
195
- -> *args { @operation.send(callable, *args) }
206
+ -> *args { @operation.send(callable, *args) }.ruby2_keywords
196
207
  else
197
208
  callable
198
209
  end
data/pathway.gemspec CHANGED
@@ -31,13 +31,15 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_dependency "dry-inflector", ">= 0.1.0"
33
33
  spec.add_dependency "contextualizer", "~> 0.0.4"
34
+ spec.add_dependency "ruby2_keywords"
34
35
 
35
36
  spec.add_development_dependency "dry-validation", ">= 0.11"
36
- spec.add_development_dependency "bundler", ">= 1.14.0"
37
- spec.add_development_dependency "sequel", "~> 5.25.0"
37
+ spec.add_development_dependency "bundler", ">= 2.4.10"
38
+ spec.add_development_dependency "sequel", "~> 5.0"
38
39
  spec.add_development_dependency "rake", "~> 13.0"
39
- spec.add_development_dependency "rspec", "~> 3.0"
40
- spec.add_development_dependency "coveralls"
40
+ spec.add_development_dependency "rspec", "~> 3.11"
41
+ spec.add_development_dependency "simplecov-lcov", '~> 0.8.0'
42
+ spec.add_development_dependency "simplecov"
41
43
  spec.add_development_dependency "pry"
42
44
  spec.add_development_dependency "pry-byebug"
43
45
  spec.add_development_dependency "pry-doc"
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.11.3
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pablo Herrero
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-22 00:00:00.000000000 Z
11
+ date: 2024-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-inflector
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.0.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby2_keywords
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: dry-validation
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -58,28 +72,28 @@ dependencies:
58
72
  requirements:
59
73
  - - ">="
60
74
  - !ruby/object:Gem::Version
61
- version: 1.14.0
75
+ version: 2.4.10
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - ">="
67
81
  - !ruby/object:Gem::Version
68
- version: 1.14.0
82
+ version: 2.4.10
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: sequel
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 5.25.0
89
+ version: '5.0'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 5.25.0
96
+ version: '5.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -100,16 +114,30 @@ dependencies:
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: '3.0'
117
+ version: '3.11'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.11'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov-lcov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.8.0
104
132
  type: :development
105
133
  prerelease: false
106
134
  version_requirements: !ruby/object:Gem::Requirement
107
135
  requirements:
108
136
  - - "~>"
109
137
  - !ruby/object:Gem::Version
110
- version: '3.0'
138
+ version: 0.8.0
111
139
  - !ruby/object:Gem::Dependency
112
- name: coveralls
140
+ name: simplecov
113
141
  requirement: !ruby/object:Gem::Requirement
114
142
  requirements:
115
143
  - - ">="
@@ -171,7 +199,7 @@ executables: []
171
199
  extensions: []
172
200
  extra_rdoc_files: []
173
201
  files:
174
- - ".circleci/config.yml"
202
+ - ".github/workflows/tests.yml"
175
203
  - ".gitignore"
176
204
  - ".rspec"
177
205
  - CHANGELOG.md
@@ -185,6 +213,8 @@ files:
185
213
  - bin/rspec
186
214
  - bin/setup
187
215
  - lib/pathway.rb
216
+ - lib/pathway/plugins/auto_deconstruct_state.rb
217
+ - lib/pathway/plugins/auto_deconstruct_state/ruby3.rb
188
218
  - lib/pathway/plugins/dry_validation.rb
189
219
  - lib/pathway/plugins/dry_validation/v0_11.rb
190
220
  - lib/pathway/plugins/dry_validation/v0_12.rb
@@ -210,7 +240,7 @@ licenses:
210
240
  metadata:
211
241
  bug_tracker_uri: https://github.com/pabloh/pathway/issues
212
242
  source_code_uri: https://github.com/pabloh/pathway
213
- post_install_message:
243
+ post_install_message:
214
244
  rdoc_options: []
215
245
  require_paths:
216
246
  - lib
@@ -225,8 +255,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
225
255
  - !ruby/object:Gem::Version
226
256
  version: '0'
227
257
  requirements: []
228
- rubygems_version: 3.0.8
229
- signing_key:
258
+ rubygems_version: 3.5.10
259
+ signing_key:
230
260
  specification_version: 4
231
261
  summary: Define your business logic in simple steps.
232
262
  test_files: []
data/.circleci/config.yml DELETED
@@ -1,38 +0,0 @@
1
- version: 2.0
2
- shared: &shared
3
- steps:
4
- - checkout
5
- - run:
6
- name: Bundle gems
7
- command: |
8
- echo '--no-rdoc --no-ri' > '.gemrc'
9
- bundle install --jobs=4 --retry=3 --path vendor/bundle
10
- - run:
11
- name: Run tests
12
- command: bundle exec rspec --format documentation --color --format progress spec
13
-
14
- jobs:
15
- "ruby-2.4":
16
- <<: *shared
17
- docker:
18
- - image: circleci/ruby:2.4
19
-
20
- "ruby-2.5":
21
- <<: *shared
22
- docker:
23
- - image: circleci/ruby:2.5
24
-
25
- "ruby-2.6":
26
- <<: *shared
27
- docker:
28
- - image: circleci/ruby:2.6
29
- environment:
30
- REPORT_COVERAGE: 'true'
31
-
32
- workflows:
33
- version: 2
34
- build:
35
- jobs:
36
- - "ruby-2.4"
37
- - "ruby-2.5"
38
- - "ruby-2.6"