action_logic 0.0.6 → 0.1.0

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
  SHA1:
3
- metadata.gz: 7abe1ab881dcbe8c41c35c71cd44936e3f9de6ba
4
- data.tar.gz: 93e727b74558560b6d31b11e60a4bd819d76ad60
3
+ metadata.gz: c17b002948787563a10bc653bf44ee73f88e989d
4
+ data.tar.gz: cf88c4dfb9009e45b222acd03b7ec3f8eef9cc89
5
5
  SHA512:
6
- metadata.gz: d4e97d382f0a5ef9dbaeeb8afe23e4ef7bc75b9d9f441fedb5ec36a8e91fe6bd5480a32e784254882e0e30215b8481b7a9931f85dcb3ebe094c323ba8359b4d8
7
- data.tar.gz: ec7ff48a690eb592f8a1d977aaae0be1759b506dccfb82d058bb6ca37666982c2526bd2dbe412d6234eeb29f1daca4bb80eb8d3efe137a36984fe9f68f18260c
6
+ metadata.gz: 47dfecfa70d4193a4c69b418f747db4bd98e17178fa5c22e3a42ee37db724c8e9d9c8bb82e66360ba4d1f02ab2ecfff0c2b2c13a53d38bd9482e8a9dda502508
7
+ data.tar.gz: d834c1c3f475660f8cb03342aae902c85b8d3c5c8675bf4574d8df0b50aa71b4a929828bb8ca5fd21739c75b46d3c7412a78816e781f13dc2ecd52f2c15bf288
@@ -0,0 +1,50 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at rick.winfrey@gmail.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+
45
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
46
+ version 1.3.0, available at
47
+ [http://contributor-covenant.org/version/1/3/0/][version]
48
+
49
+ [homepage]: http://contributor-covenant.org
50
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- action_logic (0.0.4)
4
+ action_logic (0.0.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -7,165 +7,905 @@
7
7
 
8
8
  ### Introduction
9
9
 
10
- This is a business logic abstraction gem that provides structure to the organization and composition of business logic in a Ruby or Rails application. `ActionLogic` is inspired by similar gems such as [ActiveInteraction](https://github.com/orgsync/active_interaction), [DecentExposure](https://github.com/hashrocket/decent_exposure), [Interactor](https://github.com/collectiveidea/interactor), [Light-Service](https://github.com/adomokos/light-service), [Mutations](https://github.com/cypriss/mutations), [Surrounded](https://github.com/saturnflyer/surrounded), [Trailblazer](https://github.com/apotonick/trailblazer) and [Wisper](https://github.com/krisleech/wisper).
11
-
12
- Why another business logic abstraction gem? `ActionLogic` seeks to provide teams of varying experience levels to work with a common set of abstractions that help to honor the SOLID principles, make resulting business logic code easy and simple to test and allow teams to spin up or refactor business domains quickly and efficiently.
10
+ This is a business logic abstraction gem that provides structure to the organization and composition of business logic in a Ruby or Rails application. `ActionLogic` is inspired by gems like [ActiveInteraction](https://github.com/orgsync/active_interaction), [DecentExposure](https://github.com/hashrocket/decent_exposure), [Interactor](https://github.com/collectiveidea/interactor), [Light-Service](https://github.com/adomokos/light-service), [Mutations](https://github.com/cypriss/mutations), [Surrounded](https://github.com/saturnflyer/surrounded), [Trailblazer](https://github.com/apotonick/trailblazer) and [Wisper](https://github.com/krisleech/wisper).
11
+
12
+ Why another business logic abstraction gem? `ActionLogic` provides teams of various experience levels with a minimal yet powerful set of abstractions that promote easy to write and easy to understand code. By using `ActionLogic`, teams can more quickly and easily write business logic that honors the SOLID principles, is easy to test and easy to reason about, and provides a flexible foundation from which teams can model and define their application's business domains by focusing on reusable units of work that can be composed and validated with one another.
13
+
14
+ ### Contents
15
+
16
+ * [Backstory](#backstory)
17
+ * [Overview](#overview)
18
+ * [`ActionContext`](#actioncontext)
19
+ * [`ActionTask`](#actiontask)
20
+ * [`ActionUseCase`](#actionusecase)
21
+ * [`ActionCoordinator`](#actioncoordinator)
22
+ * [Succeeding an `ActionContext`](#succeeding-an-actioncontext)
23
+ * [Failing an `ActionContext`](#failing-an-actioncontext)
24
+ * [Halting an `ActionContext`](#halting-an-actioncontext)
25
+ * [Custom `ActionContext` Status](#custom-actioncontext)
26
+ * [Error Handling](#error-handling)
27
+ * [Attribute Validations](#attribute-validations)
28
+ * [Type Validations](#type-validations)
29
+ * [Custom Type Validations](#custom-type-validations)
30
+ * [Presence Validations](#presence-validations)
31
+ * [Custom Presence Validations](#custom-presence-validations)
32
+ * [Before Validations](#before-validations)
33
+ * [After Validations](#after-validations)
34
+ * [Around Validations](#around-validations)
35
+
36
+ ### Backstory
37
+
38
+ Consider a traditional e-commerce Rails application. Users can shop online and add items to their shopping cart until they are ready to check out.
39
+ The happy path scenario might go something like this: the user submits their order form, an orders controller action records the order in the database,
40
+ submits the order total to a payment processor, waits for a response from the payment processor, and upon a success response from the payment processor sends
41
+ an order confirmation email to the user, the order is sent internally to the warehouse for fulfillment which requires creating various records in the database,
42
+ and finally the server responds to the initial POST request with a rendered html page including a message indicating the order was successfully processed. In this
43
+ work flow there are at least 7 distinct steps or tasks that must be satisfied in order for the application's business logic to be considered correct according
44
+ to specifications.
45
+
46
+ Although this flow works well for most users, there are other users whose credit card information might be expired or users who might attempt to check out when
47
+ your application's payment processor service is down. Additional edge case scenarios start to pop up in error logs as exception emails fill up your inbox.
48
+ What happens when that user that is notorious for having 100 tabs open forgets to complete the checkout process and submits a two week old order form that
49
+ includes an item that your e-commerce store no longer stocks? What happens if an item is sold out? The edge cases and exception emails pile up, and as each one comes in
50
+ you add more and more logic to that controller action.
51
+
52
+ What once was a simple controller action designed with only the happy path of a successful checkout in mind has now become 100 lines long with 5 to 10 levels
53
+ of nested if statements. The voice of Uncle Bob starts ringing in your ears and you know there must be a better way. You think on it for awhile and consider not only
54
+ the technical challenges of refactoring this code, but you'd also like to make this code reusable and modular. You want this code to be easy to test and easy to maintain.
55
+ You want to honor the SOLID principles by writing classes that are singularly focused and easy to extend. You reason these new classes should only have to change if the
56
+ business logic they execute changes. You see that there are relationships between the entities and you see the possibility of abstractions that allow entities of similar types
57
+ to interact nicely with each other. You begin thinking about interfaces and the Liskov Substitution Principle, and eventually your mind turns towards domains and data modeling.
58
+ Where does it end you wonder?
59
+
60
+ But you remember your team. It's a team of people all wanting to do their best, and represent a variety of backgrounds and experiences. Each person has varying degress of familiarity
61
+ with different types of abstractions and approaches, and you wonder what abstractions might be as easy to work with for a new developer as they are for an experienced developer?
62
+ You consider DSL's you've used in the past and wonder what is that ideal balance between magic and straightforward OOP design?
63
+
64
+ As more and more questions pile up in the empty space of your preferred text editor, you receive another exception email for a new problem with the order flow. The questions about
65
+ how to refactor this code transform into asking questions about how can you edit the existing code to add the new fix? Add a new nested if statement? You do what you can given the
66
+ constraints you're faced with, and add another 5 lines and another nested if statement. You realize there is not enough time to make this refactor happen, and you've got to push the
67
+ fix out as soon as possible. Yet, as you merge your feature branch in master and deploy a hotfix, you think surely there must be a better way.
68
+
69
+ `ActionLogic` was born from many hours thinking about these questions and considering how it might be possible to achieve a generic set of abstractions to help guide
70
+ business logic that would promote the SOLID principles and be easy for new and experienced developers to understand and extend. It's not a perfect abstraction (as nothing is),
71
+ but *can* help simplify your application's business logic by encouraging you to consider the smallest units of work required for your business logic while offering features
72
+ like type and presence validation that help reduce or eliminate boiler plate, defensive code (nil checks anyone?). However, as with all general purpose libraries, your mileage
73
+ will vary.
13
74
 
14
75
  ### Overview
15
76
 
16
77
  There are three levels of abstraction provided by `ActionLogic`:
17
78
 
18
- * `ActionTask` (the core unit of work)
19
- * `ActionUseCase` (contains one or many `ActionTask`s)
20
- * `ActionCoordinator` (contains two or more `ActionUseCase`s)
79
+ * [`ActionTask` (a concrete unit of work)](#action_task)
80
+ * [`ActionUseCase` (organizes two or more `ActionTasks`)](#action_use_case)
81
+ * [`ActionCoordinator` (coordinates two or more `ActionUseCases`)](#action_coordinator)
82
+
83
+ Each level of abstraction operates with a shared, mutable data structure referred to as a `context` and is an instance of `ActionContext`. This shared `context` is threaded
84
+ through each `ActionTask`, `ActionUseCase` and / or `ActionCoordinator` until all work is completed. The resulting `context` is returned to the original caller
85
+ (typically in a Rails application this will be a controller action). In the problem described above we might have an `ActionUseCase` for organizing the checkout order flow,
86
+ and each of the distinct steps would be represented by a separate `ActionTask`. However, overtime it may make more sense to split apart the singular `ActionUseCase` for the order
87
+ flow into smaller `ActionUseCases` that are isolated by their domain (users, payment processor, inventory / warehouse, email, etc.). Considering that we limit our `ActionUseCases` to
88
+ single domains, then the `ActionCoordinator` abstraction would allow us to coordinate communication between the `ActionUseCases` and their `ActionTasks` to fulfill the necessary
89
+ work required when a user submits a checkout order form.
90
+
91
+ The diagram below illustrates how the `ActionTask`, `ActionUseCase` and `ActionCoordinator` abstractions work together, and the role of `ActionContext` as the primary, single input:
92
+
93
+ <img src="https://raw.githubusercontent.com/rewinfrey/action_logic/master/resources/overview_diagram.png" />
94
+
95
+ ### ActionContext
96
+
97
+ The glue that binds the three layers of abstraction provided in `ActionLogic` is `ActionContext`. Anytime an `ActionTask`, `ActionUseCase` or `ActionCoordinator` is invoked
98
+ an instance of `ActionContext` is created and passed as an input parameter to the receiving execution context. Because each of the three abstractions works in the same way
99
+ with `ActionContext`, it is intended to be a relatively simple "learn once understand everywhere" abstraction.
100
+
101
+ Instances of `ActionContext` are always referred to within the body of `call` methods defined in any `ActionTask`, `ActionUseCase` or `ActionCoordinator` as `context`. An
102
+ instance of `ActionContext` is a thin wrapper around Ruby's standard library [`OpenStruct`](http://ruby-doc.org/stdlib-2.0.0/libdoc/ostruct/rdoc/OpenStruct.html). This allows
103
+ instances of `ActionContext` to be maximally flexible. Arbitrary attributes can be defined on a `context` and their values can be of any type.
104
+
105
+ In addition to allowing arbitrary attributes and values to be defined on a `context`, instances of `ActionContext` also conform to a set of simple rules:
106
+
107
+ * Every `context` instance is instantiated with a default `status` of `:success`
108
+ * A `context` responds to `success?` which returns true if the `status` is `:success`
109
+ * A `context` responds to `fail!` which sets the `status` to `:failure`
110
+ * A `context` responds to `fail?` which returns true if the `status` is `:failure`
111
+ * A `context` rseponds to `halt!` which sets the `status` to `:halted`
112
+ * A `context` responds to `halted?` which returns true if the `status` is `:halted`
113
+
114
+ Enough with the words, let's look at some code! The following shows an instance of `ActionContext` and its various abilities:
115
+
116
+ ```ruby
117
+ context = ActionLogic::ActionContext.new
118
+
119
+ context # => #<ActionLogic::ActionContext status=:success>
120
+
121
+ # default status is `:success`:
122
+ context.status # => :success
123
+
124
+ # defining a new attribute called `name` with the value `"Example"`:
125
+ context.name = "Example"
126
+
127
+ # retrieving the value of the `name` attribute:
128
+ context.name # => "Example"
129
+
130
+ # you can set attributes to anything, including Procs:
131
+ context.lambda_example = -> { "here" }
132
+
133
+ context.lambda_example # => #<Proc:0x007f8d6b0a9ba0@-:11 (lambda)>
134
+
135
+ context.lambda_example.call # => "here"
21
136
 
22
- Each level of abstraction operates with a shared, mutable data structure referred to as a `context` and is an instance of `ActionContext`. This shared `context` is threaded through each `ActionTask`, `ActionUseCase` and / or `ActionCoordinator` until all work in the defined business logic flow are completed and the resulting `context` is returned to the original caller (typically in a Rails application this will be a controller action).
137
+ # contexts can be failed:
138
+ context.fail!
139
+
140
+ context.status # => :failure
141
+
142
+ context.failure? # => true
143
+
144
+ # contexts can also be halted:
145
+ context.halt!
146
+
147
+ context.status # => :halted
148
+
149
+ context.halted? # => true
150
+ ```
151
+
152
+ Now that we have seen what `ActionContext` can do, let's take a look at the lowest level of absraction in `ActionLogic` that consumes instances of `ActionContext`, the `ActionTask`
153
+ abstraction.
23
154
 
24
155
  ### ActionTask
25
156
 
26
- At the core of every `ActionLogic` work flow is an `ActionTask`. These units of work represent where concrete work is performed. All `ActionTask`s conform to the same basic structure and incorporate all the features of `ActionLogic` including validations, error handling and the ability to mutate the shared `context` made available to the `ActionTask`.
157
+ At the core of every `ActionLogic` work flow is an `ActionTask`. These classes are the lowest level of abstraction in `ActionLogic` and are where concrete work is performed. All `ActionTasks` conform to the same structure and incorporate all features of `ActionLogic` including validations and error handling.
27
158
 
28
- The following is a simple example of an `ActionTask`:
159
+ To implement an `ActionTask` class you must define a `call` method. You can also specify any before, after or around validations or an error handler. The following code example demonstrates an `ActionTask` class that includes before and after validations, and also demonstrates how an `ActionTask` is invoked :
29
160
 
30
161
  ```ruby
31
162
  class ActionTaskExample
32
163
  include ActionLogic::ActionTask
33
-
164
+
165
+ validates_before :expected_attribute1 => { :type => :string },
166
+ :expected_attribute2 => { :type => :integer, :presence => true }
167
+ validates_after :example_attribute1 => { :type => :string, :presence => ->(example_attribute1) { !example_attribute1.empty? } }
168
+
34
169
  def call
35
- context.example_attribute1 = "Example value"
36
- context.example_attribute2 = 123
170
+ # adds `example_attribute1` to the shared `context` with the value "Example value"
171
+ context.example_attribute1 = "New value from context attributes: #{context.expected_attribute1} #{context.expected_attribute2}"
37
172
  end
38
173
  end
39
- ```
40
174
 
41
- To invoke the above `ActionTask`:
175
+ # ActionTasks are invoked by calling an `execute` static method directly on the class with an optional hash of key value pairs:
176
+ result = ActionTaskExample.execute(:expected_attribute1 => "example", :expected_attribute2 => 123)
42
177
 
43
- ```ruby
44
- result = ActionTaskExample.execute
45
- result # => <ActionContext :success=true, :example_attribute1="Example value", :example_attribute2=123, :message = "">
178
+ # The result object is the shared context object (an instance of ActionContext):
179
+ result # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_attribute1="New value from context attributes: example 123">
46
180
  ```
47
181
 
48
- This is a simple example, but shows the basic structure of `ActionTask`s and the way they can be invoked by themselves in isolation. However, many of the business logic work flows we find ourselves needing in Rails applications require multiple steps or tasks to achieve the intended result. When we have a business workflow that requires multiple tasks, we can use the `ActionUseCase` abstraction to provide organization and a deterministic order for how the required `ActionTask`s are invoked.
182
+ The `ActionTaskExample` is invoked using the static method `execute` which takes an optional hash of attributes that is converted into an `ActionContext`. Assuming the before validations are satisfied, the `call` method is invoked. In the body of the `call` method the `ActionTask` can access the shared `ActionContext` instance via a `context` object. This shared `context` object allows for getting and setting attributes as needed. When the `call` method returns, the `context` is validated against any defined after validations, and the `context` is then returned to the caller.
183
+
184
+ The diagram below is a visual representation of how an `ActionTask` is evaluted when its `execute` method is invoked from a caller:
185
+
186
+ <img src="https://raw.githubusercontent.com/rewinfrey/action_logic/master/resources/action_task_diagram.png" />
187
+
188
+ Although this example is for the `ActionTask` abstraction, `ActionUseCase` and `ActionCoordinator` follow the same pattern. The difference is that `ActionUseCase` is designed to organize multiple `ActionTasks`, and `ActionCoordinator` is designed to organize many `ActionUseCases`.
49
189
 
50
190
  ### ActionUseCase
51
191
 
52
- Most of the time our business logic work flows can be thought of as use cases of a given domain in our Rails application. Whether that domain is a user, account or notification domain, we can abstract a series of steps that need to be performed from a controller action into a well defined use case that specifies a series of tasks in order to satisfy that use case's goal. `ActionUseCase` represents a layer of abstraction that organizes multiple `ActionTask`s and executes them in a specified order with a shared `context`:
192
+ As business logic grows in complexity the number of steps or tasks required to fulfill that business logic tends to increase. Managing this complexity is a problem every team must face. Abstractions can help teams of varying experience levels work together and promote code that remains modular and simple to understand and extend. `ActionUseCase` represents a layer of abstraction that organizes multiple `ActionTasks` and executes each `ActionTask` in the order they are defined. Each task receives the same shared `context` so tasks can be composed together.
193
+
194
+ To implement an `ActionUseCase` class you must define a `call` method and a `tasks` method. You also can specify any before, after or around validations or an error handler. The following is an example showcasing how an `ActionUseCase` class organizes the execution of multiple `ActionTasks` and defines before and after validations on the shared `context`:
53
195
 
54
196
  ```ruby
55
197
  class ActionUseCaseExample
56
198
  include ActionLogic::ActionUseCase
57
-
199
+
200
+ validates_before :expected_attribute1 => { :type => :string },
201
+ :expected_attribute2 => { :type => :integer, :presence => true }
202
+ validates_after :example_task1 => { :type => :boolean, :presence => true },
203
+ :example_task2 => { :type => :boolean, :presence => true },
204
+ :example_task3 => { :type => :boolean, :presence => true },
205
+ :example_usecase1 => { :type => :boolean, :presence => true }
206
+
58
207
  # The `call` method is invoked prior to invoking any of the ActionTasks defined by the `tasks` method.
59
208
  # The purpose of the `call` method allows us to prepare the shared `context` prior to invoking the ActionTasks.
60
209
  def call
61
- context.example_attribute1 = "Example value"
62
- context.example_attribute2 = 123
210
+ context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success>
211
+ context.example_usecase1 = true
63
212
  end
64
-
213
+
65
214
  def tasks
66
215
  [ActionTaskExample1,
67
216
  ActionTaskExample2,
68
217
  ActionTaskExample3]
69
218
  end
70
219
  end
220
+
221
+ class ActionTaskExample1
222
+ include ActionLogic::ActionTask
223
+ validates_after :example_task1 => { :type => :boolean, :presence => true }
224
+
225
+ def call
226
+ context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true>
227
+ context.example_task1 = true
228
+ end
229
+ end
230
+
231
+ class ActionTaskExample2
232
+ include ActionLogic::ActionTask
233
+ validates_after :example_task2 => { :type => :boolean, :presence => true }
234
+
235
+ def call
236
+ context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true, example_task1=true>
237
+ context.example_task2 = true
238
+ end
239
+ end
240
+
241
+ class ActionTaskExample3
242
+ include ActionLogic::ActionTask
243
+ validates_after :example_task3 => { :type => :boolean, :presence => true }
244
+
245
+ def call
246
+ context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true, example_task1=true, example_task2=true>
247
+ context.example_task3 = true
248
+ end
249
+ end
250
+
251
+ # To invoke the ActionUseCaseExample, we call its execute method with the required attributes:
252
+ result = ActionUseCaseExample.execute(:expected_attribute1 => "example", :expected_attribute2 => 123)
253
+
254
+ result # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true, example_task1=true, example_task2=true, example_task3=true>
255
+ ```
256
+
257
+ By following the value of the shared `context` from the `ActionUseCaseExample` to each of the `ActionTask` classes, it is possible to see how the shared `context` is mutated to accomodate the various attributes and their values each execution context adds to the `context`. It also reveals the order in which the `ActionTasks` are evaluated, and indicates that the `call` method of the `ActionUseCaseExample` is invoked prior to any of the `ActionTasks` defined in the `tasks` method.
258
+
259
+ To help visualize the flow of execution when an `ActionUseCase` is invoked, this diagram aims to illustrate the relationship between `ActionUseCase` and `ActionTasks` and the order in which operations are performed:
260
+
261
+ <img src="https://raw.githubusercontent.com/rewinfrey/action_logic/master/resources/action_use_case_diagram.png" />
262
+
263
+ ### ActionCoordinator
264
+
265
+ Sometimes the behavior we wish our Ruby or Rails application to provide requires us to coordinate work between various domains of our application's business logic. The `ActionCoordinator` abstraction is intended to help coordinate multiple `ActionUseCases` by allowing you to define a plan of which `ActionUseCases` to invoke depending on the outcome of each `ActionUseCase` execution. The `ActionCoordinator` abstraction is the highest level of abstraction in `ActionLogic`.
266
+
267
+ To implement an `ActionCoordinator` class, you must define a `call` method in addition to a `plan` method. The purpose of the `plan` method is to define a state transition map that links together the various `ActionUseCase` classes the `ActionCoordinator` is organizing, as well as allowing you to define error or halt scenarios based on the result of each `ActionUseCase`. The following code example demonstrates a simple `ActionCoordinator`:
268
+
269
+ ```ruby
270
+ class ActionCoordinatorExample
271
+ include ActionLogic::ActionCoordinator
272
+
273
+ def call
274
+ context.required_attribute1 = "required attribute 1"
275
+ context.required_attribute2 = "required attribute 2"
276
+ end
277
+
278
+ def plan
279
+ {
280
+ ActionUseCaseExample1 => { :success => ActionUseCaseExample2,
281
+ :failure => ActionUseCaseFailureExample },
282
+ ActionUseCaseExample2 => { :success => nil,
283
+ :failure => ActionUseCaseFailureExample },
284
+ ActionUseCaseFailureExample => { :success => nil }
285
+ }
286
+ end
287
+ end
288
+
289
+ class ActionUseCaseExample1
290
+ include ActionLogic::ActionUseCase
291
+
292
+ validates_before :required_attribute1 => { :type => :string }
293
+
294
+ def call
295
+ context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2">
296
+ context.example_usecase1 = true
297
+ end
298
+
299
+ # Normally `tasks` would define multiple tasks, but in this example, I've used one ActionTask to keep the overall code example smaller
300
+ def tasks
301
+ [ActionTaskExample1]
302
+ end
303
+ end
304
+
305
+ class ActionUseCaseExample2
306
+ include ActionLogic::ActionUseCase
307
+
308
+ validates_before :required_attribute2 => { :type => :string }
309
+
310
+ def call
311
+ context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2", example_usecase1=true, example_task1=true>
312
+ context.example_usecase2 = true
313
+ end
314
+
315
+ # Normally `tasks` would define multiple tasks, but in this example, I've used one ActionTask to keep the overall code example smaller
316
+ def tasks
317
+ [ActionTaskExample2]
318
+ end
319
+ end
320
+
321
+ # In this example, we are not calling ActionUseCaseFailureExample, but is used to illustrate the purpose of the `plan` of our ActionCoordinator
322
+ # in the event of a failure in one of the consumed `ActionUseCases`
323
+ class ActionUseCaseFailureExample
324
+ include ActionLogic::ActionUseCase
325
+
326
+ def call
327
+ end
328
+
329
+ def tasks
330
+ [ActionTaskLogFailure,
331
+ ActionTaskEmailFailure]
332
+ end
333
+ end
334
+
335
+ class ActionTaskExample1
336
+ include ActionLogic::ActionTask
337
+ validates_after :example_task1 => { :type => :boolean, :presence => true }
338
+
339
+ def call
340
+ context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2", example_usecase1=true>
341
+ context.example_task1 = true
342
+ end
343
+ end
344
+
345
+ class ActionTaskExample2
346
+ include ActionLogic::ActionTask
347
+ validates_after :example_task2 => { :type => :boolean, :presence => true }
348
+
349
+ def call
350
+ context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2", example_usecase1=true, example_task1=true, example_usecase2=true>
351
+ context.example_task2 = true
352
+ end
353
+ end
354
+
355
+ result = ActionCoordinatorExample.execute
356
+
357
+ result # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2", example_usecase1=true, example_task1=true, example_usecase2=true, example_task2=true>
71
358
  ```
72
359
 
73
- We see in the above example that an `ActionUseCase` differs from `ActionTask` by adding a `tasks` method. The `tasks` method defines a list of `ActionTask` classes that are invoked in order with the same shared `context` passed from task1 to task2 and so on until all tasks are invoked. Additionally, `ActionUseCase` requires us to define a `call` method that allows us to prepare any necessary attributes and values on the shared `context` prior to beginning the evaluation of the `ActionTask`s defined by the `tasks` method.
360
+ <img src="https://raw.githubusercontent.com/rewinfrey/action_logic/master/resources/action_coordinator_diagram.png" />
74
361
 
75
- We can invoke the above `ActionUseCase` in the following way:
362
+ ### Succeeding an `ActionContext`
363
+ By default, the value of the `status` attribute of instances of `ActionContext` is `:success`. Normally this is useful information for the caller of an `ActionTask`, `ActionUseCase` or `ActionCoordinator`
364
+ because it informs the caller that the various execution context(s) were successful. In other words, a `:success` status indicates that none of the execution contexts had a failure
365
+ or halted execution.
366
+
367
+ ### Failing an `ActionContext`
368
+ Using `context.fail!` does two important things: it immediately stops the execution of any proceeding business logic (prevents any additional `ActionTasks` from executing)
369
+ and also sets the status of the `context` as `:failure`. This status is most applicable to the caller or an `ActionCoordinator` that might have a plan specifically for a `:failure`
370
+ status of a resulting `ActionUseCase`.
371
+
372
+ The following is a simple example to show how a `context` is failed within a `call` method:
76
373
 
77
374
  ```ruby
78
- ActionUseCaseExample.execute()
375
+ class ActionTaskExample
376
+ include ActionLogic::ActionTask
377
+
378
+ def call
379
+ if failure_condition?
380
+ context.fail!
381
+ end
382
+ end
383
+
384
+ def failure_condition?
385
+ true
386
+ end
387
+ end
388
+
389
+ result = ActionTaskExample.execute
390
+
391
+ result # => #<ActionLogic::ActionContext status=:failure, message="">
79
392
  ```
80
393
 
81
- ### ActionCoordinator
394
+ When failing a `context` it is possible to also specify a message:
82
395
 
83
- ### ActionContext
396
+ ```ruby
397
+ class ActionTaskExample
398
+ include ActionLogic::ActionTask
399
+
400
+ def call
401
+ if failure_condition?
402
+ context.fail! "Something was invalid"
403
+ end
404
+ end
405
+
406
+ def failure_condition?
407
+ true
408
+ end
409
+ end
410
+
411
+ result = ActionTaskExample.execute
412
+
413
+ result # => #<ActionLogic::ActionContext status=:failure, message="Something was invalid">
414
+
415
+ result.message # => "Something was invalid"
416
+ ```
417
+
418
+ From the above example we see how it is possible to `fail!` a `context` while also specifying a clarifying message about the failure condition. Later, we retrieve
419
+ that failure message via the `message` attribute defined on the returned `context`.
420
+
421
+ ### Halting an `ActionContext`
422
+ Like, failing a context, Using `context.halt!` does two important things: it immediately halts the execution of any proceeding business logic (prevents any additional `ActionTasks`
423
+ from executing) and also sets the status of the `context` as `:halted`. The caller may use that information to define branching logic or an `ActionCoordinator` may use that
424
+ information as part of its `plan`.
425
+
426
+ However, unlike failing a `context`, halting is designed to indicate that no more processing is required, but otherwise execution was successful.
427
+
428
+ The following is a simple example to show how a `context` is halted within a `call` method:
429
+
430
+ ```ruby
431
+ class ActionTaskExample
432
+ include ActionLogic::ActionTask
433
+
434
+ def call
435
+ if halt_condition?
436
+ context.halt!
437
+ end
438
+ end
439
+
440
+ def halt_condition?
441
+ true
442
+ end
443
+ end
444
+
445
+ result = ActionTaskExample.execute
446
+
447
+ result # => #<ActionLogic::ActionContext status=:halted, message="">
448
+ ```
449
+
450
+ When failing a `context` it is possible to also specify a message:
451
+
452
+ ```ruby
453
+ class ActionTaskExample
454
+ include ActionLogic::ActionTask
455
+
456
+ def call
457
+ if halt_condition?
458
+ context.halt! "Something required a halt"
459
+ end
460
+ end
461
+
462
+ def halt_condition?
463
+ true
464
+ end
465
+ end
466
+
467
+ result = ActionTaskExample.execute
468
+
469
+ result # => #<ActionLogic::ActionContext status=:halted, message="Something required a halt">
470
+
471
+ result.message # => "Something required a halt"
472
+ ```
473
+
474
+ From the above example we see how it is possible to `halt!` a `context` while also specifying a clarifying message about the halt condition. Later, we retrieve
475
+ that halt message via the `message` attribute defined on the returned `context`.
476
+
477
+ ### Custom `ActionContext` Status
478
+ It is worthwhile to point out that you should not feel limited to only using the three provided statuses of `:success`, `:failure` or `:halted`. It is easy to implement your
479
+ own system of statuses if you prefer. For example, consider a system that is used to defining various status codes or disposition codes to indicate the result of some business
480
+ logic. Instances of `ActionContext` can be leveraged to indicate these disposition codes by using the `status` attribute, or by defining custom attributes. You are encouraged
481
+ to expirement and play with the flexibility provided to you by `ActionContext` in determining what is optimal for your given code contexts and your team.
482
+
483
+ ```ruby
484
+ class RailsControllerExample < ApplicationController
485
+ def create
486
+ case create_use_case.status
487
+ when :disposition_1 then ActionUseCaseSuccess1.execute(create_use_case)
488
+ when :disposition_2 then ActionUseCaseSuccess2.execute(create_use_case)
489
+ when :disposition_9 then ActionUseCaseFailure.execute(create_use_case)
490
+ else
491
+ ActionUseCaseDefault.execute(create_use_case)
492
+ end
493
+ end
494
+
495
+ private
496
+
497
+ def create_use_case
498
+ @create_use_case ||= ActionUseCaseExample.execute(params)
499
+ end
500
+ end
501
+ ```
502
+
503
+ Although this contrived example would be ideal for an `ActionCoordinator` (because the result of `ActionUseCaseExample` drives the execution of the next `ActionUseCase`), this
504
+ example serves to show that `status` can be used with custom disposition codes to drive branching behavior.
505
+
506
+ ### Error Handling
507
+ During execution of an `ActionTask`, `ActionUseCase` or `ActionCoordinator` you may wish to define custom behavior for handling errors. Within any of these classes
508
+ you can define an `error` method that receives as its input the error exception. Invoking an `error` method does not make any assumptions about the `status` of the
509
+ underlying `context`. Execution of the `ActionTask`, `ActionUseCase` or `ActionCoordinator` also stops after the `error` method returns, and execution of the work
510
+ flow continues as normal unless the `context` is failed or halted.
511
+
512
+ The following example is a simple illustration of how an `error` method is invoked for an `ActionTask`:
513
+
514
+ ```ruby
515
+ class ActionTaskExample
516
+ include ActionLogic::ActionTask
517
+
518
+ def call
519
+ context.before_raise = true
520
+ raise "Something broke"
521
+ context.after_raise = true
522
+ end
523
+
524
+ def error(e)
525
+ context.error = "the error is passed in as an input parameter: #{e.class}"
526
+ end
527
+ end
528
+
529
+ result = ActionTaskExample.execute
84
530
 
85
- ### Features
531
+ # the status of the context is not mutated
532
+ result.status # => :success
86
533
 
87
- `ActionLogic` provides a number of convenience functionality that supports simple to complex business logic work flows while maintaining a simple and easy to understand API:
534
+ result.error # => "the error is passed in as an input parameter: RuntimeError"
88
535
 
89
- * Validations (`context` is verified to have all necessary attributes, have `presence` and are of the correct type)
90
- * Custom error handling defined as a callback
91
- * Prematurely halt or fail a workflow
536
+ result.before_raise # => true
92
537
 
93
- ### Validations
538
+ result.after_raise # => nil
539
+ ```
540
+
541
+ It is important to note that defining an `error` method is **not** required. If at any point in the execution of an `ActionTask`, `ActionUseCase` or `ActionCoordinator`
542
+ an uncaught exception is thrown **and** an `error` method is **not** defined, the exception is raised to the caller.
543
+
544
+ ### Attribute Validations
545
+ The most simple and basic type of validation offered by `ActionLogic` is attribute validation. To require that an attribute be defined on an instance of `ActionContext`, you
546
+ need only specify the name of the attribute and an empty hash with one of the three validation types (before, after or around):
547
+
548
+ ```ruby
549
+ class ActionTaskExample
550
+ include ActionLogic::ActionTask
551
+
552
+ validates_before :required_attribute1 => {}
553
+
554
+ def call
555
+ end
556
+ end
557
+
558
+ result = ActionTaskExample.execute(:required_attribute1 => true)
559
+
560
+ result.status # => :success
561
+
562
+ result.required_attribute1 # => true
563
+ ```
564
+
565
+ However, in the above example, if we were to invoke the `ActionTaskExample` without the `required_attribute1` parameter, the before validation would fail and raise
566
+ an `ActionLogic::MissingAttributeError` and also detail which attribute is missing:
567
+
568
+ ```ruby
569
+ class ActionTaskExample
570
+ include ActionLogic::ActionTask
571
+
572
+ validates_before :required_attribute1 => {}
573
+
574
+ def call
575
+ end
576
+ end
577
+
578
+ ActionTaskExample.execute # ~> [:required_attribute1] (ActionLogic::MissingAttributeError)
579
+ ```
580
+
581
+ Attribute validations are defined in the same way regardless of the timing of the validation ([before](#before-validations), [after](#after-validations) or
582
+ [around](#around-validations)). Please refer to the relevant sections for examples of their usage.
583
+
584
+ ### Type Validations
585
+ In addition to attribute validations, `ActionLogic` also allows you to validate against the type of the value of the attribute you expect to be defined in an instance
586
+ of `ActionContext`. To understand the default types `ActionLogic` validates against, please see the following example:
587
+
588
+ ```ruby
589
+ class ActionTaskExample
590
+ include ActionLogic::ActionTask
591
+
592
+ validates_after :integer_test => { :type => :integer },
593
+ :float_test => { :type => :float },
594
+ :string_test => { :type => :string },
595
+ :bool_test => { :type => :boolean },
596
+ :hash_test => { :type => :hash },
597
+ :array_test => { :type => :array },
598
+ :symbol_test => { :type => :symbol },
599
+ :nil_test => { :type => :nil }
600
+
601
+ def call
602
+ context.integer_test = 123
603
+ context.float_test = 1.0
604
+ context.string_test = "test"
605
+ context.bool_test = true
606
+ context.hash_test = {}
607
+ context.array_test = []
608
+ context.symbol_test = :symbol
609
+ context.nil_test = nil
610
+ end
611
+ end
612
+
613
+ result = ActionTaskExample.execute
614
+
615
+ result # => #<ActionLogic::ActionContext status=:success,
616
+ # integer_test=123,
617
+ # float_test=1.0,
618
+ # string_test="test",
619
+ # bool_test=true,
620
+ # hash_test={},
621
+ # array_test=[],
622
+ # symbol_test=:symbol,
623
+ # nil_test=nil>
624
+ ```
625
+
626
+ It's important to point out that Ruby's `true` and `false` are not `Boolean` but `TrueClass` and `FalseClass` respectively. Additionally, `nil`'s type is `NilClass` in Ruby.
627
+ To simplify the way these validations work for `true` or `false`, type validations expect the symbol `:boolean` as the `:type`. `nil` is validated simply with the `:nil` `:type`.
628
+
629
+ As we saw with attribute validations, if an attribute's value does not conform to the type expected, `ActionLogic` will raise an `ActionLogic::AttributeTypeError`
630
+ with a detailed description about which attribute's value failed the validation:
631
+
632
+ ```ruby
633
+ class ActionTaskExample
634
+ include ActionLogic::ActionTask
635
+
636
+ validates_after :integer_test => { :type => :integer }
637
+
638
+ def call
639
+ context.integer_test = 1.0
640
+ end
641
+ end
642
+
643
+ ActionTaskExample.execute # ~> ["Attribute: integer_test with value: 1.0 was expected to be of type integer but is float"] (ActionLogic::AttributeTypeError)
644
+ ```
645
+
646
+ In addition to the above default types it is possible to also validate against user defined types.
647
+
648
+ ### Custom Type Validations
649
+ If you would like to validate the type of attributes on a given `context` with your application's classes, `ActionLogic` is happy to provide that functionality.
650
+
651
+ Let's consider the following example:
652
+
653
+ ```ruby
654
+ class ExampleClass
655
+ end
656
+
657
+ class ActionTaskExample
658
+ include ActionLogic::ActionTask
659
+
660
+ validates_after :example_attribute => { :type => :exampleclass }
661
+
662
+ def call
663
+ context.example_attribute = ExampleClass.new
664
+ end
665
+ end
666
+
667
+ result = ActionTaskExample.execute
668
+
669
+ result # => #<ActionLogic::ActionContext status=:success, example_attribute=#<ExampleClass:0x007f95d1922bd8>>
670
+ ```
671
+
672
+ In the above example, a custom class `ExampleClass` is defined. In order to type validate against this class, the required format for the name of the class is simply
673
+ the lowercase version of the class as a symbol. `ExampleClass` becomes `:exampleclass`, `UserAttributes` becomes `:userattributes`,
674
+ `ReallyLongClassNameThatBreaks80ColumnsInVimRule` becomes `:reallylongclassnamethatbreaks80columnsinvimrule` and so on.
675
+
676
+ If a custom type validation fails, `ActionLogic` provides the same `ActionLogic::AttributeTypeError` with a detailed explanation about what attribute is in violation
677
+ of the type validation:
678
+
679
+ ```ruby
680
+ class ExampleClass
681
+ end
682
+
683
+ class OtherClass
684
+ end
685
+
686
+ class ActionTaskExample
687
+ include ActionLogic::ActionTask
688
+
689
+ validates_after :example_attribute => { :type => :exampleclass }
690
+
691
+ def call
692
+ context.example_attribute = OtherClass.new
693
+ end
694
+ end
695
+
696
+ ActionTaskExample.execute # ~> ["Attribute: example_attribute with value: #<OtherClass:0x007fb5ca04edb8> was expected to be of type exampleclass but is otherclass"] (ActionLogic::AttributeTypeError)
697
+ ```
698
+
699
+ Attribute and type validations are very helpful, but in some situations this is not enough. Additionally, `ActionLogic` provides presence validation so you can also verify that
700
+ a given attribute on a context not only has the correct type, but also has a value that is considered `present`.
701
+
702
+ ### Presence Validations
703
+
704
+ `ActionLogic` also allows for presence validation for any attribute on an instance of `ActionContext`. Like other validations, presence validations can be specified in before, after or
705
+ around validations.
706
+
707
+ By default, presence validations simply check to determine if an attribute's value is not `nil` or is not `false`. To define a presence validation, you need only specify `:presence => true`
708
+ for the attribute you wish to validate against:
709
+
710
+ ```ruby
711
+ class ActionTaskExample
712
+ include ActionLogic::ActionTask
713
+
714
+ validates_before :example_attribute => { :presence => true }
715
+
716
+ def call
717
+ end
718
+ end
94
719
 
95
- Validating that a shared `context` contains the necessary attributes (parameters) becomes increasingly important as your application grows in complexity and `ActionTask` or `ActionUseCase` classes are reused. `ActionLogic` makes it easy to validate your shared `context`s by providing three different validations:
720
+ result = ActionTaskExample.execute(:example_attribute => 123)
96
721
 
97
- * Attribute is defined on a context
98
- * Attribute has a value (presence)
99
- * Attribute has the correct type
722
+ result # => #<ActionLogic::ActionContext example_attribute=123, status=:success>
723
+ ```
724
+
725
+ However, if a presence validation fails, `ActionLogic` will raise an `ActionLogic::PresenceError` with a detailed description about the attribute failing the presence validation
726
+ and why:
727
+
728
+ ```ruby
729
+ class ActionTaskExample
730
+ include ActionLogic::ActionTask
731
+
732
+ validates_before :example_attribute => { :presence => true }
733
+
734
+ def call
735
+ end
736
+ end
100
737
 
101
- Additionally, validations can be invoked in three ways in any execution context (`ActionTask`, `ActionUseCase` or `ActionCoordinator`):
738
+ ActionTaskExample.execute(:example_attribute => nil) # ~> ["Attribute: example_attribute is missing value in context but presence validation was specified"] (ActionLogic::PresenceError)
739
+ ```
102
740
 
103
- * Before validations are invoked before the execution context is invoked
104
- * After validations are invoked after the execution context is invoked
105
- * Aroud validations are invoked before and after the execution context is invoked
741
+ ### Custom Presence Validations
106
742
 
107
- Validations are defined and made available for all execution contexts with the same methods and format:
743
+ Sometimes when wanting to validate presence of an attribute with an aggregate type (like `Array` or `Hash`), we may want to validate that such a type is not empty. If
744
+ you wish to validate presence for a type that requires inspecting the value of the attribute, `ActionLogic` allows you the ability to define a custom `Proc` to validate
745
+ an attribute's value against.
108
746
 
109
747
  ```ruby
110
- class ExampleActionTask
748
+ class ActionTaskExample
111
749
  include ActionLogic::ActionTask
112
-
113
- validates_before :attribute1 => { :type => :integer, :presence => true },
114
- :attribute2 => { :type => :string, :presence => true }
115
-
116
- validates_after :attribute3 => { :type => :boolean, :presence => true },
117
- :attribute4 => { :type => :string, :presence => true }
118
-
119
- validates_around :ids => { :type => :array, :presence => ->(ids) { !ids.empty? } }
120
-
750
+
751
+ validates_before :example_attribute => { :presence => ->(attribute) { attribute.any? } }
752
+
121
753
  def call
122
- # set attribute3 on the shared context to satisfy the `validates_after` validations
123
- context.attribute3 = true
124
-
125
- # set attribute4 on the shared context to satisfy the `validates_after` validations
126
- context.attribute4 = "an example string value"
127
754
  end
128
755
  end
129
756
 
130
- # In order to satisfy ExampleActionTask's `validates_before` validations, we must provide an initial
131
- # hash of attributes and values that satisfy the `validates_before` validations:
132
- params = {
133
- :attribute1 => 1,
134
- :attribute2 => "another example string value"
135
- }
757
+ result = ActionTaskExample.execute(:example_attribute => ["element1", "element2", "element3"])
758
+
759
+ result # => #<ActionLogic::ActionContext example_attribute=["element1", "element2", "element3"], status=:success>
760
+ ```
761
+
762
+ In the example above, we define a lambda that accepts as input the value of the attribute on the `context`. In this case, we are interested in verifying that
763
+ `example_attribute` is not an empty `Array` or an empty `Hash`. This passes our before validation because `ActionTaskExample` is invoked with an `example_attribute`
764
+ whose value is an array consisting of three elements.
765
+
766
+ However, if a custom presence validation fails, `ActionLogic` will raise an `ActionLogic::PresenceError` with a detailed description about the attribute failing
767
+ the custom presence validation:
768
+
769
+ ```ruby
770
+ class ActionTaskExample
771
+ include ActionLogic::ActionTask
136
772
 
137
- # In order to satisfy ExampleActionTask's `validates_around` validation, we must provide an initial
138
- # attribute and value that will satisfy the `validates_around` validation:
139
- params[:ids] = [1, 2, 3, 4]
773
+ validates_before :example_attribute => { :presence => ->(attribute) { attribute.any? } }
140
774
 
141
- ExampleActionTask.execute(params) # => <ActionContext :success=true, :attribute1=1, :attribute2="another example string value", :attribute3=true, :attribute4="an example string value", :ids=[1,2,3,4] :message="">
775
+ def call
776
+ end
777
+ end
778
+
779
+ ActionTaskExample.execute(:example_attribute => []) # ~> ["Attribute: example_attribute is missing value in context but custom presence validation was specified"] (ActionLogic::PresenceError)
142
780
  ```
143
781
 
144
- ### Supported Types For Validation
782
+ In the above example, we have failed to pass the presence validation for `example_attribute` because the value of `example_attribute` is an empty array. When
783
+ the custom presence validation lambda is called, the lambda returns `false` and the `ActionLogic::PresenceError` is thrown, with an error message indicating
784
+ the attribute that failed the presence validation while also indicating that a custom presence validation was specified.
145
785
 
146
- `ActionLogic` supports the following built in Ruby data types:
786
+ ### Before Validations
147
787
 
148
- * :string
149
- * :boolean (rather than TrueClass or FalseClass)
150
- * :float
151
- * :integer (rather than FixNum)
152
- * :array
153
- * :hash
154
- * :nil (rather than NilClass)
788
+ If you combine Rails ActionController's `before_filter` and ActiveModel's `validates` then you have approximately what `ActionLogic` defines as `validates_before`.
789
+ Before validations can be defined in any of the `ActionLogic` abstractions (`ActionTask`, `ActionUseCase` and `ActionCoordinator`). In each abstraction a `validates_before`
790
+ operation is performed *before* invoking the `call` method.
155
791
 
156
- Additionally, `ActionLogic` allows you to also validate user defined types (custom types):
792
+ Before validations allow you to specify a required attribute and optionally its type and / or presence. The following example illustrates how to specify a before
793
+ validation on a single attribute:
157
794
 
158
795
  ```ruby
796
+ class ActionTaskExample
797
+ include ActionLogic::ActionTask
798
+
799
+ validates_before :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } }
159
800
 
160
- class CustomType1
801
+ def call
802
+ end
161
803
  end
162
804
 
163
- class ExampleActionTask
805
+ result = ActionTaskExample.execute(:example_attribute => [1, 2, 3])
806
+
807
+ result # => #<ActionLogic::ActionContext example_attribute=[1, 2, 3], status=:success>
808
+ ```
809
+
810
+ The following example illustrates how to specify a before validation for multiple attributes:
811
+
812
+ ```ruby
813
+ class ActionTaskExample
164
814
  include ActionLogic::ActionTask
165
-
166
- :validates_before { :custom_type_attribute => { :type => :customtype1 } }
167
-
815
+
816
+ validates_before :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } },
817
+ :example_attribute2 => { :type => :integer }
818
+
168
819
  def call
169
820
  end
170
821
  end
822
+
823
+ result = ActionTaskExample.execute(:example_attribute => [1, 2, 3], :example_attribute2 => 1)
824
+
825
+ result # => #<ActionLogic::ActionContext example_attribute=[1, 2, 3], example_attribute2=1, status=:success>
826
+ ```
827
+
828
+ ### After Validations
829
+
830
+ If you combine Rails ActionController's `after_filter` and ActiveModel's `validates` then you have approximately what `ActionLogic` defines as `validates_after`.
831
+ After validations can be defined in any of the `ActionLogic` abstractions (`ActionTask`, `ActionUseCase` and `ActionCoordinator`). In each abstraction a `validates_after`
832
+ operation is performed *after* invoking the `call` method.
833
+
834
+ After validations allow you to specify a required attribute and optionally its type and / or presence. The following example illustrates how to specify an after
835
+ validation on a single attribute:
836
+
837
+ ```ruby
838
+ class ActionTaskExample
839
+ include ActionLogic::ActionTask
840
+
841
+ validates_after :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } }
842
+
843
+ def call
844
+ context.example_attribute = [1, 2, 3]
845
+ end
846
+ end
847
+
848
+ result = ActionTaskExample.execute
849
+
850
+ result # => #<ActionLogic::ActionContext example_attribute=[1, 2, 3], status=:success>
851
+ ```
852
+ The following example illustrates how to specify an after validation for multiple attributes:
853
+
854
+ ```ruby
855
+ class ActionTaskExample
856
+ include ActionLogic::ActionTask
857
+
858
+ validates_after :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } },
859
+ :example_attribute2 => { :type => :integer }
860
+
861
+ def call
862
+ context.example_attribute = [1, 2, 3]
863
+ context.example_attribute2 = 1
864
+ end
865
+ end
866
+
867
+ result = ActionTaskExample.execute
868
+
869
+ result # => #<ActionLogic::ActionContext example_attribute=[1, 2, 3], example_attribute2=1, status=:success>
870
+ ```
871
+
872
+ ### Around Validations
873
+
874
+ If you combine Rails ActionController's `around_filter` and ActiveModel's `validates` then you have approximately what `ActionLogic` defines as `validates_around`.
875
+ Around validations can be defined in any of the `ActionLogic` abstractions (`ActionTask`, `ActionUseCase` and `ActionCoordinator`). In each abstraction a `validates_around`
876
+ operation is performed *before* and *after* invoking the `call` method.
877
+
878
+ Around validations allow you to specify a required attribute and optionally its type and / or presence. The following example illustrates how to specify an around
879
+ validation on a single attribute:
880
+
881
+ ```ruby
882
+ class ActionTaskExample
883
+ include ActionLogic::ActionTask
884
+
885
+ validates_around :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } }
886
+
887
+ def call
888
+ end
889
+ end
890
+
891
+ result = ActionTaskExample.execute(:example_attribute => [1, 2, 3])
892
+
893
+ result # => #<ActionLogic::ActionContext example_attribute=[1, 2, 3], status=:success>
894
+ ```
895
+ The following example illustrates how to specify an around validation for multiple attributes:
896
+
897
+ ```ruby
898
+ class ActionTaskExample
899
+ include ActionLogic::ActionTask
900
+
901
+ validates_around :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } },
902
+ :example_attribute2 => { :type => :integer }
903
+
904
+ def call
905
+ end
906
+ end
907
+
908
+ result = ActionTaskExample.execute(:example_attribute => [1, 2, 3], :example_attribute2 => 1)
909
+
910
+ result # => #<ActionLogic::ActionContext example_attribute=[1, 2, 3], example_attribute2=1, status=:success>
171
911
  ```