├── .github
└── workflows
│ └── test.yaml
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── Gemfile.lock
├── README.md
├── action_logic.gemspec
├── lib
├── action_logic.rb
└── action_logic
│ ├── action_benchmark.rb
│ ├── action_benchmark
│ ├── default_benchmark_block.rb
│ └── default_formatter.rb
│ ├── action_context.rb
│ ├── action_coordinator.rb
│ ├── action_core.rb
│ ├── action_includes.rb
│ ├── action_task.rb
│ ├── action_use_case.rb
│ ├── action_validation.rb
│ ├── action_validation
│ ├── attribute_validation.rb
│ ├── base_validation.rb
│ ├── presence_validation.rb
│ └── type_validation.rb
│ ├── configuration.rb
│ ├── errors.rb
│ └── version.rb
├── resources
├── action_coordinator_diagram.png
├── action_task_diagram.png
├── action_use_case_diagram.png
├── diagrams.sketch
└── overview_diagram.png
└── spec
├── action_logic
├── action_benchmark
│ └── default_formatter_spec.rb
├── action_context_spec.rb
├── action_coordinator_spec.rb
├── action_task_spec.rb
├── action_use_case_spec.rb
└── configuration_spec.rb
├── fixtures
├── constants.rb
├── coordinators.rb
├── custom_types.rb
├── tasks.rb
└── use_cases.rb
└── spec_helper.rb
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | strategy:
6 | fail-fast: false
7 | matrix:
8 | os: [ubuntu-latest, macos-latest]
9 | # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0'
10 | ruby: ['2.7', '3.0', '3.1']
11 | runs-on: ${{ matrix.os }}
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: ruby/setup-ruby@v1
15 | with:
16 | ruby-version: ${{ matrix.ruby }}
17 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
18 | - run: bundle exec rspec spec
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | *.gem
3 | benchmark.log
4 | tags
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - "2.1.0"
4 | - "2.2.0"
5 | - "2.3.0"
6 | script: bundle exec rspec spec
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
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/
51 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to ActionLogic
2 | ===========================
3 |
4 | Thank you for your interest in contributing! You're encouraged to submit [pull requests](https://github.com/rewinfrey/actionlogic/pulls),
5 | [propose features and discuss issues](https://github.com/rewinfrey/actionlogic/issues). When in doubt, ask a question in the form of an
6 | [issue](https://github.com/rewinfrey/actionlogic/issues).
7 |
8 | #### Fork the Project
9 |
10 | Fork the [project on Github](https://github.com/rewinfrey/actionlogic) and check out your copy.
11 |
12 | ```
13 | git clone https://github.com/contributor/actionlogic.git
14 | cd actionlogic
15 | git remote add upstream https://github.com/rewinfrey/actionlogic.git
16 | ```
17 |
18 | #### Create a Feature Branch
19 |
20 | Make sure your fork is up-to-date and create a feature branch for your feature or bug fix.
21 |
22 | ```
23 | git checkout master
24 | git pull upstream master
25 | git checkout -b my-feature-branch
26 | ```
27 |
28 | #### Bundle Install and Test
29 |
30 | Ensure that you can build the project and run tests.
31 |
32 | ```
33 | bundle
34 | bundle exec rspec spec
35 | ```
36 |
37 | #### Write Tests
38 |
39 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [specs](https://github.com/rewinfrey/ActionLogic/tree/master/spec).
40 |
41 | Pull requests with specs that highlight or reproduce a problem, even without a fix, are very much appreciated and welcomed!
42 |
43 | #### Write Code
44 |
45 | Implement your feature or bug fix.
46 |
47 | Make sure that `bundle exec rspec spec` completes without errors.
48 |
49 | #### Write Documentation
50 |
51 | Document any external behavior in the [README](README.md).
52 |
53 | #### Commit Changes
54 |
55 | Make sure git knows your name and email address:
56 |
57 | ```
58 | git config --global user.name "Your Name"
59 | git config --global user.email "contributor@example.com"
60 | ```
61 |
62 | Writing good commit logs is important. A commit log should describe what changed and why.
63 |
64 | ```
65 | git add ...
66 | git commit
67 | ```
68 |
69 | #### Push
70 |
71 | ```
72 | git push origin my-feature-branch
73 | ```
74 |
75 | #### Make a Pull Request
76 |
77 | Go to https://github.com/contributor/ActionLogic and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days.
78 |
79 | #### Check on Your Pull Request
80 |
81 | Go back to your pull request after a few minutes and see whether the Travis-CI builds are all passing. Everything should look green, otherwise fix issues and add your fix as new commits (no need
82 | to rebase or squash commits).
83 |
84 | #### Thank You
85 |
86 | Any and all contributions are very appreciated!
87 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | gemspec
3 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | action_logic (0.3.3)
5 |
6 | GEM
7 | remote: https://rubygems.org/
8 | specs:
9 | codecov (0.6.0)
10 | simplecov (>= 0.15, < 0.22)
11 | coderay (1.1.3)
12 | diff-lcs (1.5.0)
13 | docile (1.4.0)
14 | method_source (1.0.0)
15 | pry (0.14.1)
16 | coderay (~> 1.1)
17 | method_source (~> 1.0)
18 | rake (13.0.6)
19 | rspec (3.11.0)
20 | rspec-core (~> 3.11.0)
21 | rspec-expectations (~> 3.11.0)
22 | rspec-mocks (~> 3.11.0)
23 | rspec-core (3.11.0)
24 | rspec-support (~> 3.11.0)
25 | rspec-expectations (3.11.0)
26 | diff-lcs (>= 1.2.0, < 2.0)
27 | rspec-support (~> 3.11.0)
28 | rspec-mocks (3.11.1)
29 | diff-lcs (>= 1.2.0, < 2.0)
30 | rspec-support (~> 3.11.0)
31 | rspec-support (3.11.0)
32 | simplecov (0.21.2)
33 | docile (~> 1.1)
34 | simplecov-html (~> 0.11)
35 | simplecov_json_formatter (~> 0.1)
36 | simplecov-html (0.12.3)
37 | simplecov_json_formatter (0.1.4)
38 |
39 | PLATFORMS
40 | ruby
41 |
42 | DEPENDENCIES
43 | action_logic!
44 | codecov (~> 0.6.0)
45 | pry (~> 0.14.1)
46 | rake (~> 13.0.6)
47 | rspec (~> 3.11)
48 | simplecov (~> 0.21.2)
49 |
50 | BUNDLED WITH
51 | 1.17.2
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ActionLogic
2 |
3 | [](https://travis-ci.org/rewinfrey/ActionLogic)
4 | [](https://badge.fury.io/rb/action_logic)
5 | [](https://codeclimate.com/github/rewinfrey/action_logic)
6 | [](https://codecov.io/github/rewinfrey/ActionLogic?branch=master)
7 | [](http://opensource.org/licenses/MIT)
8 |
9 | ### Introduction
10 |
11 | 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).
12 |
13 | 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.
14 |
15 | ### Contents
16 |
17 | * [Backstory](#backstory)
18 | * [Overview](#overview)
19 | * [`ActionContext`](#actioncontext)
20 | * [`ActionTask`](#actiontask)
21 | * [`ActionUseCase`](#actionusecase)
22 | * [`ActionCoordinator`](#actioncoordinator)
23 | * [Succeeding an `ActionContext`](#succeeding-an-actioncontext)
24 | * [Failing an `ActionContext`](#failing-an-actioncontext)
25 | * [Halting an `ActionContext`](#halting-an-actioncontext)
26 | * [Custom `ActionContext` Status](#custom-actioncontext)
27 | * [Error Handling](#error-handling)
28 | * [Attribute Validations](#attribute-validations)
29 | * [Type Validations](#type-validations)
30 | * [Custom Type Validations](#custom-type-validations)
31 | * [Presence Validations](#presence-validations)
32 | * [Custom Presence Validations](#custom-presence-validations)
33 | * [Before Validations](#before-validations)
34 | * [After Validations](#after-validations)
35 | * [Around Validations](#around-validations)
36 | * [Benchmarking](#benchmarking)
37 | * [Enable Benchmarking](#enable-benchmarking)
38 | * [Benchmark Logging](#benchmark-logging)
39 | * [Benchmark Log Formatting](#benchmark-log-formatting)
40 | * [Custom Benchmark Handling](#custom-benchmark-handling)
41 | * [Installation](#installation)
42 | * [Contributing](#contributing)
43 |
44 | ### Backstory
45 |
46 | 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.
47 | 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,
48 | 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
49 | 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,
50 | 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
51 | 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
52 | to specifications.
53 |
54 | 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
55 | 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.
56 | 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
57 | 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
58 | you add more and more logic to that controller action.
59 |
60 | 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
61 | of nested if statements. You think on it for awhile and consider not only the technical challenges of refactoring this code, but you'd also like to make this code
62 | reusable and modular. You want this code to be easy to test and easy to maintain. You want to honor the SOLID principles by writing classes that are singularly focused
63 | and easy to extend. You reason these new classes should only have to change if the business logic they execute changes. You see that there are relationships between the
64 | entities and you see the possibility of abstractions that allow entities of similar types to interact nicely with each other. You begin thinking about interfaces and the
65 | Liskov Substitution Principle, and eventually your mind turns towards domains and data modeling. Where does it end you wonder?
66 |
67 | 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
68 | 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?
69 | You consider DSL's you've used in the past and wonder what is that ideal balance between magic and straightforward OOP design?
70 |
71 | 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
72 | 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
73 | 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
74 | 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.
75 |
76 | `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
77 | 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),
78 | 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
79 | 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
80 | will vary.
81 |
82 | ### Overview
83 |
84 | There are three levels of abstraction provided by `ActionLogic`:
85 |
86 | * [`ActionTask` (a concrete unit of work)](#action_task)
87 | * [`ActionUseCase` (organizes two or more `ActionTasks`)](#action_use_case)
88 | * [`ActionCoordinator` (coordinates two or more `ActionUseCases`)](#action_coordinator)
89 |
90 | 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
91 | through each `ActionTask`, `ActionUseCase` and / or `ActionCoordinator` until all work is completed. The resulting `context` is returned to the original caller
92 | (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,
93 | 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
94 | flow into smaller `ActionUseCases` that are isolated by their domain (users, payment processor, inventory / warehouse, email, etc.). Considering that we limit our `ActionUseCases` to
95 | single domains, then the `ActionCoordinator` abstraction would allow us to coordinate communication between the `ActionUseCases` and their `ActionTasks` to fulfill the necessary
96 | work required when a user submits a checkout order form.
97 |
98 | The diagram below illustrates how the `ActionTask`, `ActionUseCase` and `ActionCoordinator` abstractions work together, and the role of `ActionContext` as the primary, single input:
99 |
100 |
101 |
102 | ### ActionContext
103 |
104 | The glue that binds the three layers of abstraction provided in `ActionLogic` is `ActionContext`. Anytime an `ActionTask`, `ActionUseCase` or `ActionCoordinator` is invoked
105 | 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
106 | with `ActionContext`, it is intended to be a relatively simple "learn once understand everywhere" abstraction.
107 |
108 | Instances of `ActionContext` are always referred to within the body of `call` methods defined in any `ActionTask`, `ActionUseCase` or `ActionCoordinator` as `context`. An
109 | 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
110 | instances of `ActionContext` to be maximally flexible. Arbitrary attributes can be defined on a `context` and their values can be of any type.
111 |
112 | 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:
113 |
114 | * Every `context` instance is instantiated with a default `status` of `:success`
115 | * A `context` responds to `success?` which returns true if the `status` is `:success`
116 | * A `context` responds to `fail!` which sets the `status` to `:failure`
117 | * A `context` responds to `fail?` which returns true if the `status` is `:failure`
118 | * A `context` rseponds to `halt!` which sets the `status` to `:halted`
119 | * A `context` responds to `halted?` which returns true if the `status` is `:halted`
120 |
121 | Enough with the words, let's look at some code! The following shows an instance of `ActionContext` and its various abilities:
122 |
123 | ```ruby
124 | context = ActionLogic::ActionContext.new
125 |
126 | context # => #
127 |
128 | # default status is `:success`:
129 | context.status # => :success
130 |
131 | # defining a new attribute called `name` with the value `"Example"`:
132 | context.name = "Example"
133 |
134 | # retrieving the value of the `name` attribute:
135 | context.name # => "Example"
136 |
137 | # you can set attributes to anything, including Procs:
138 | context.lambda_example = -> { "here" }
139 |
140 | context.lambda_example # => #
141 |
142 | context.lambda_example.call # => "here"
143 |
144 | # contexts can be failed:
145 | context.fail!
146 |
147 | context.status # => :failure
148 |
149 | context.failure? # => true
150 |
151 | # contexts can also be halted:
152 | context.halt!
153 |
154 | context.status # => :halted
155 |
156 | context.halted? # => true
157 | ```
158 |
159 | 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`
160 | abstraction.
161 |
162 | ### ActionTask
163 |
164 | 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.
165 |
166 | 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 :
167 |
168 | ```ruby
169 | class ActionTaskExample
170 | include ActionLogic::ActionTask
171 |
172 | validates_before :expected_attribute1 => { :type => String },
173 | :expected_attribute2 => { :type => Fixnum, :presence => true }
174 | validates_after :example_attribute1 => { :type => String, :presence => ->(example_attribute1) { !example_attribute1.empty? } }
175 |
176 | def call
177 | # adds `example_attribute1` to the shared `context` with the value "Example value"
178 | context.example_attribute1 = "New value from context attributes: #{context.expected_attribute1} #{context.expected_attribute2}"
179 | end
180 | end
181 |
182 | # ActionTasks are invoked by calling an `execute` static method directly on the class with an optional hash of key value pairs:
183 | result = ActionTaskExample.execute(:expected_attribute1 => "example", :expected_attribute2 => 123)
184 |
185 | # The result object is the shared context object (an instance of ActionContext):
186 | result # => #
187 | ```
188 |
189 | The `ActionTaskExample` is invoked using the static method `execute` which takes an optional hash of attributes that is converted into an `ActionContext`.
190 | 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`
191 | instance via a `context` object. This shared `context` object allows for getting and setting attributes as needed. When the `call` method returns, the `context`
192 | is validated against any defined after validations, and the `context` is then returned to the caller.
193 |
194 | The diagram below is a visual representation of how an `ActionTask` is evaluted when its `execute` method is invoked from a caller:
195 |
196 |
197 |
198 | Although this example is for the `ActionTask` abstraction, `ActionUseCase` and `ActionCoordinator` follow the same pattern. The difference is that `ActionUseCase`
199 | is designed to organize multiple `ActionTasks`, and `ActionCoordinator` is designed to organize many `ActionUseCases`.
200 |
201 | ### ActionUseCase
202 |
203 | 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.
204 | 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
205 | 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.
206 |
207 | 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.
208 | 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`:
209 |
210 | ```ruby
211 | class ActionUseCaseExample
212 | include ActionLogic::ActionUseCase
213 |
214 | validates_before :expected_attribute1 => { :type => String },
215 | :expected_attribute2 => { :type => Fixnum, :presence => true }
216 | validates_after :example_task1 => { :type => TrueClass, :presence => true },
217 | :example_task2 => { :type => TrueClass, :presence => true },
218 | :example_task3 => { :type => TrueClass, :presence => true },
219 | :example_usecase1 => { :type => TrueClass, :presence => true }
220 |
221 | # The `call` method is invoked prior to invoking any of the ActionTasks defined by the `tasks` method.
222 | # The purpose of the `call` method allows us to prepare the shared `context` prior to invoking the ActionTasks.
223 | def call
224 | context # => #
225 | context.example_usecase1 = true
226 | end
227 |
228 | def tasks
229 | [ActionTaskExample1,
230 | ActionTaskExample2,
231 | ActionTaskExample3]
232 | end
233 | end
234 |
235 | class ActionTaskExample1
236 | include ActionLogic::ActionTask
237 | validates_after :example_task1 => { :type => TrueClass, :presence => true }
238 |
239 | def call
240 | context # => #
241 | context.example_task1 = true
242 | end
243 | end
244 |
245 | class ActionTaskExample2
246 | include ActionLogic::ActionTask
247 | validates_after :example_task2 => { :type => TrueClass, :presence => true }
248 |
249 | def call
250 | context # => #
251 | context.example_task2 = true
252 | end
253 | end
254 |
255 | class ActionTaskExample3
256 | include ActionLogic::ActionTask
257 | validates_after :example_task3 => { :type => TrueClass, :presence => true }
258 |
259 | def call
260 | context # => #
261 | context.example_task3 = true
262 | end
263 | end
264 |
265 | # To invoke the ActionUseCaseExample, we call its execute method with the required attributes:
266 | result = ActionUseCaseExample.execute(:expected_attribute1 => "example", :expected_attribute2 => 123)
267 |
268 | result # => #
269 | ```
270 |
271 | 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`
272 | 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`
273 | are evaluated, and indicates that the `call` method of the `ActionUseCaseExample` is invoked prior to any of the `ActionTasks` defined in the `tasks` method.
274 |
275 | To help visualize the flow of execution when an `ActionUseCase` is invoked, this diagram aims to illustrate the relationship between `ActionUseCase` and `ActionTasks`
276 | and the order in which operations are performed:
277 |
278 |
279 |
280 | ### ActionCoordinator
281 |
282 | 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.
283 | The `ActionCoordinator` abstraction is intended to help coordinate multiple `ActionUseCases` by allowing you to define a plan of which `ActionUseCases` to invoke
284 | depending on the outcome of each `ActionUseCase` execution. The `ActionCoordinator` abstraction is the highest level of abstraction in `ActionLogic`.
285 |
286 | 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
287 | transition map that links together the various `ActionUseCase` classes the `ActionCoordinator` is organizing, as well as allowing you to define error or halt
288 | scenarios based on the result of each `ActionUseCase`. The following code example demonstrates a simple `ActionCoordinator`:
289 |
290 | ```ruby
291 | class ActionCoordinatorExample
292 | include ActionLogic::ActionCoordinator
293 |
294 | def call
295 | context.required_attribute1 = "required attribute 1"
296 | context.required_attribute2 = "required attribute 2"
297 | end
298 |
299 | def plan
300 | {
301 | ActionUseCaseExample1 => { :success => ActionUseCaseExample2,
302 | :failure => ActionUseCaseFailureExample },
303 | ActionUseCaseExample2 => { :success => nil,
304 | :failure => ActionUseCaseFailureExample },
305 | ActionUseCaseFailureExample => { :success => nil }
306 | }
307 | end
308 | end
309 |
310 | class ActionUseCaseExample1
311 | include ActionLogic::ActionUseCase
312 |
313 | validates_before :required_attribute1 => { :type => String }
314 |
315 | def call
316 | context # => #
317 | context.example_usecase1 = true
318 | end
319 |
320 | # Normally `tasks` would define multiple tasks, but in this example, I've used one ActionTask to keep the overall code example smaller
321 | def tasks
322 | [ActionTaskExample1]
323 | end
324 | end
325 |
326 | class ActionUseCaseExample2
327 | include ActionLogic::ActionUseCase
328 |
329 | validates_before :required_attribute2 => { :type => String }
330 |
331 | def call
332 | context # => #
333 | context.example_usecase2 = true
334 | end
335 |
336 | # Normally `tasks` would define multiple tasks, but in this example, I've used one ActionTask to keep the overall code example smaller
337 | def tasks
338 | [ActionTaskExample2]
339 | end
340 | end
341 |
342 | # In this example, we are not calling ActionUseCaseFailureExample, but is used to illustrate the purpose of the `plan` of our ActionCoordinator
343 | # in the event of a failure in one of the consumed `ActionUseCases`
344 | class ActionUseCaseFailureExample
345 | include ActionLogic::ActionUseCase
346 |
347 | def call
348 | end
349 |
350 | def tasks
351 | [ActionTaskLogFailure,
352 | ActionTaskEmailFailure]
353 | end
354 | end
355 |
356 | class ActionTaskExample1
357 | include ActionLogic::ActionTask
358 | validates_after :example_task1 => { :type => TrueClass, :presence => true }
359 |
360 | def call
361 | context # => #
362 | context.example_task1 = true
363 | end
364 | end
365 |
366 | class ActionTaskExample2
367 | include ActionLogic::ActionTask
368 | validates_after :example_task2 => { :type => TrueClass, :presence => true }
369 |
370 | def call
371 | context # => #
372 | context.example_task2 = true
373 | end
374 | end
375 |
376 | result = ActionCoordinatorExample.execute
377 |
378 | result # => #
379 | ```
380 |
381 |
382 |
383 | ### Succeeding an `ActionContext`
384 | 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`,
385 | `ActionUseCase` or `ActionCoordinator` because it informs the caller that the various execution context(s) were successful. In other words, a `:success` status
386 | indicates that none of the execution contexts had a failure or halted execution.
387 |
388 | ### Failing an `ActionContext`
389 | Using `context.fail!` does two important things: it immediately stops the execution of any proceeding business logic (prevents any additional `ActionTasks` from executing)
390 | 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`
391 | status of a resulting `ActionUseCase`.
392 |
393 | The following is a simple example to show how a `context` is failed within a `call` method:
394 |
395 | ```ruby
396 | class ActionTaskExample
397 | include ActionLogic::ActionTask
398 |
399 | def call
400 | if failure_condition?
401 | context.fail!
402 | end
403 | end
404 |
405 | def failure_condition?
406 | true
407 | end
408 | end
409 |
410 | result = ActionTaskExample.execute
411 |
412 | result # => #
413 | ```
414 |
415 | When failing a `context` it is possible to also specify a message:
416 |
417 | ```ruby
418 | class ActionTaskExample
419 | include ActionLogic::ActionTask
420 |
421 | def call
422 | if failure_condition?
423 | context.fail! "Something was invalid"
424 | end
425 | end
426 |
427 | def failure_condition?
428 | true
429 | end
430 | end
431 |
432 | result = ActionTaskExample.execute
433 |
434 | result # => #
435 |
436 | result.message # => "Something was invalid"
437 | ```
438 |
439 | 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
440 | that failure message via the `message` attribute defined on the returned `context`.
441 |
442 | ### Halting an `ActionContext`
443 | 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`
444 | 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
445 | information as part of its `plan`.
446 |
447 | However, unlike failing a `context`, halting is designed to indicate that no more processing is required, but otherwise execution was successful.
448 |
449 | The following is a simple example to show how a `context` is halted within a `call` method:
450 |
451 | ```ruby
452 | class ActionTaskExample
453 | include ActionLogic::ActionTask
454 |
455 | def call
456 | if halt_condition?
457 | context.halt!
458 | end
459 | end
460 |
461 | def halt_condition?
462 | true
463 | end
464 | end
465 |
466 | result = ActionTaskExample.execute
467 |
468 | result # => #
469 | ```
470 |
471 | When failing a `context` it is possible to also specify a message:
472 |
473 | ```ruby
474 | class ActionTaskExample
475 | include ActionLogic::ActionTask
476 |
477 | def call
478 | if halt_condition?
479 | context.halt! "Something required a halt"
480 | end
481 | end
482 |
483 | def halt_condition?
484 | true
485 | end
486 | end
487 |
488 | result = ActionTaskExample.execute
489 |
490 | result # => #
491 |
492 | result.message # => "Something required a halt"
493 | ```
494 |
495 | 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
496 | that halt message via the `message` attribute defined on the returned `context`.
497 |
498 | ### Custom `ActionContext` Status
499 | 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
500 | 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
501 | 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
502 | 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.
503 |
504 | ```ruby
505 | class RailsControllerExample < ApplicationController
506 | def create
507 | case create_use_case.status
508 | when :disposition_1 then ActionUseCaseSuccess1.execute(create_use_case)
509 | when :disposition_2 then ActionUseCaseSuccess2.execute(create_use_case)
510 | when :disposition_9 then ActionUseCaseFailure.execute(create_use_case)
511 | else
512 | ActionUseCaseDefault.execute(create_use_case)
513 | end
514 | end
515 |
516 | private
517 |
518 | def create_use_case
519 | @create_use_case ||= ActionUseCaseExample.execute(params)
520 | end
521 | end
522 | ```
523 |
524 | Although this contrived example would be ideal for an `ActionCoordinator` (because the result of `ActionUseCaseExample` drives the execution of the next `ActionUseCase`), this
525 | example serves to show that `status` can be used with custom disposition codes to drive branching behavior.
526 |
527 | ### Error Handling
528 | During execution of an `ActionTask`, `ActionUseCase` or `ActionCoordinator` you may wish to define custom behavior for handling errors. Within any of these classes
529 | 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
530 | underlying `context`. Execution of the `ActionTask`, `ActionUseCase` or `ActionCoordinator` also stops after the `error` method returns, and execution of the work
531 | flow continues as normal unless the `context` is failed or halted.
532 |
533 | The following example is a simple illustration of how an `error` method is invoked for an `ActionTask`:
534 |
535 | ```ruby
536 | class ActionTaskExample
537 | include ActionLogic::ActionTask
538 |
539 | def call
540 | context.before_raise = true
541 | raise "Something broke"
542 | context.after_raise = true
543 | end
544 |
545 | def error(e)
546 | context.error = "the error is passed in as an input parameter: #{e.class}"
547 | end
548 | end
549 |
550 | result = ActionTaskExample.execute
551 |
552 | # the status of the context is not mutated
553 | result.status # => :success
554 |
555 | result.error # => "the error is passed in as an input parameter: RuntimeError"
556 |
557 | result.before_raise # => true
558 |
559 | result.after_raise # => nil
560 | ```
561 |
562 | 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`
563 | an uncaught exception is thrown **and** an `error` method is **not** defined, the exception is raised to the caller.
564 |
565 | ### Attribute Validations
566 | 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
567 | need only specify the name of the attribute and an empty hash with one of the three validation types (before, after or around):
568 |
569 | ```ruby
570 | class ActionTaskExample
571 | include ActionLogic::ActionTask
572 |
573 | validates_before :required_attribute1 => {}
574 |
575 | def call
576 | end
577 | end
578 |
579 | result = ActionTaskExample.execute(:required_attribute1 => true)
580 |
581 | result.status # => :success
582 |
583 | result.required_attribute1 # => true
584 | ```
585 |
586 | However, in the above example, if we were to invoke the `ActionTaskExample` without the `required_attribute1` parameter, the before validation would fail and raise
587 | an `ActionLogic::MissingAttributeError` and also detail which attribute is missing:
588 |
589 | ```ruby
590 | class ActionTaskExample
591 | include ActionLogic::ActionTask
592 |
593 | validates_before :required_attribute1 => {}
594 |
595 | def call
596 | end
597 | end
598 |
599 | ActionTaskExample.execute # ~> context: ActionTaskExample message: [:required_attribute1] (ActionLogic::MissingAttributeError)
600 | ```
601 |
602 | Attribute validations are defined in the same way regardless of the timing of the validation ([before](#before-validations), [after](#after-validations) or
603 | [around](#around-validations)). Please refer to the relevant sections for examples of their usage.
604 |
605 | ### Type Validations
606 | 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
607 | of `ActionContext`. To understand the default types `ActionLogic` validates against, please see the following example:
608 |
609 | ```ruby
610 | class ActionTaskExample
611 | include ActionLogic::ActionTask
612 |
613 | validates_after :integer_test => { :type => Fixnum },
614 | :float_test => { :type => Float },
615 | :string_test => { :type => String },
616 | :truthy_test => { :type => TrueClass },
617 | :falsey_test => { :type => FalseClass },
618 | :hash_test => { :type => Hash },
619 | :array_test => { :type => Array },
620 | :symbol_test => { :type => Symbol },
621 | :nil_test => { :type => NilClass }
622 |
623 | def call
624 | context.integer_test = 123
625 | context.float_test = 1.0
626 | context.string_test = "test"
627 | context.truthy_test = true
628 | context.falsey_test = false
629 | context.hash_test = {}
630 | context.array_test = []
631 | context.symbol_test = :symbol
632 | context.nil_test = nil
633 | end
634 | end
635 |
636 | result = ActionTaskExample.execute
637 |
638 | result # => #
648 | ```
649 |
650 | 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.
651 | Also potentially surprising to some is that Ruby's integer type is of class `Fixnum`, but floats are of class `Float`.
652 |
653 | As we saw with attribute validations, if an attribute's value does not conform to the type expected, `ActionLogic` will raise an `ActionLogic::AttributeTypeError`
654 | with a detailed description about which attribute's value failed the validation:
655 |
656 | ```ruby
657 | class ActionTaskExample
658 | include ActionLogic::ActionTask
659 |
660 | validates_after :integer_test => { :type => Fixnum }
661 |
662 | def call
663 | context.integer_test = 1.0
664 | end
665 | end
666 |
667 | ActionTaskExample.execute # ~> context: ActionTaskExample message: Attribute: integer_test with value: 1.0 was expected to be of type Fixnum but is Float (ActionLogic::AttributeTypeError)
668 | ```
669 |
670 | In addition to the above default types it is possible to also validate against user defined types.
671 |
672 | ### Custom Type Validations
673 | 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.
674 |
675 | Let's consider the following example:
676 |
677 | ```ruby
678 | class ExampleClass
679 | end
680 |
681 | class ActionTaskExample
682 | include ActionLogic::ActionTask
683 |
684 | validates_after :example_attribute => { :type => ExampleClass }
685 |
686 | def call
687 | context.example_attribute = ExampleClass.new
688 | end
689 | end
690 |
691 | result = ActionTaskExample.execute
692 |
693 | result # => #>
694 | ```
695 |
696 | 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
697 | the class constant `ExampleClass`.
698 |
699 | If a custom type validation fails, `ActionLogic` provides the same `ActionLogic::AttributeTypeError` with a detailed explanation about what attribute is in violation
700 | of the type validation:
701 |
702 | ```ruby
703 | class ExampleClass
704 | end
705 |
706 | class OtherClass
707 | end
708 |
709 | class ActionTaskExample
710 | include ActionLogic::ActionTask
711 |
712 | validates_after :example_attribute => { :type => ExampleClass }
713 |
714 | def call
715 | context.example_attribute = OtherClass.new
716 | end
717 | end
718 |
719 | ActionTaskExample.execute # ~> context: ActionTaskExample message: Attribute: example_attribute with value: # was expected to be of type ExampleClass but is OtherClass (ActionLogic::AttributeTypeError)
720 | ```
721 |
722 | 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
723 | a given attribute on a context not only has the correct type, but also has a value that is considered `present`.
724 |
725 | ### Presence Validations
726 |
727 | `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
728 | around validations.
729 |
730 | 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`
731 | for the attribute you wish to validate against:
732 |
733 | ```ruby
734 | class ActionTaskExample
735 | include ActionLogic::ActionTask
736 |
737 | validates_before :example_attribute => { :presence => true }
738 |
739 | def call
740 | end
741 | end
742 |
743 | result = ActionTaskExample.execute(:example_attribute => 123)
744 |
745 | result # => #
746 | ```
747 |
748 | However, if a presence validation fails, `ActionLogic` will raise an `ActionLogic::PresenceError` with a detailed description about the attribute failing the presence validation
749 | and why:
750 |
751 | ```ruby
752 | class ActionTaskExample
753 | include ActionLogic::ActionTask
754 |
755 | validates_before :example_attribute => { :presence => true }
756 |
757 | def call
758 | end
759 | end
760 |
761 | ActionTaskExample.execute(:example_attribute => nil) # ~> context: ActionTaskExample message: Attribute: example_attribute is missing value in context but presence validation was specified (ActionLogic::PresenceError)
762 | ```
763 |
764 | ### Custom Presence Validations
765 |
766 | 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
767 | 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
768 | an attribute's value against.
769 |
770 | ```ruby
771 | class ActionTaskExample
772 | include ActionLogic::ActionTask
773 |
774 | validates_before :example_attribute => { :presence => ->(attribute) { attribute.any? } }
775 |
776 | def call
777 | end
778 | end
779 |
780 | result = ActionTaskExample.execute(:example_attribute => ["element1", "element2", "element3"])
781 |
782 | result # => #
783 | ```
784 |
785 | 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
786 | `example_attribute` is not an empty `Array` or an empty `Hash`. This passes our before validation because `ActionTaskExample` is invoked with an `example_attribute`
787 | whose value is an array consisting of three elements.
788 |
789 | However, if a custom presence validation fails, `ActionLogic` will raise an `ActionLogic::PresenceError` with a detailed description about the attribute failing
790 | the custom presence validation:
791 |
792 | ```ruby
793 | class ActionTaskExample
794 | include ActionLogic::ActionTask
795 |
796 | validates_before :example_attribute => { :presence => ->(attribute) { attribute.any? } }
797 |
798 | def call
799 | end
800 | end
801 |
802 | ActionTaskExample.execute(:example_attribute => []) # ~> context: ActionTaskExample message: Attribute: example_attribute is missing value in context but custom presence validation was specified (ActionLogic::PresenceError)
803 | ```
804 |
805 | 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
806 | the custom presence validation lambda is called, the lambda returns `false` and the `ActionLogic::PresenceError` is thrown, with an error message indicating
807 | the attribute that failed the presence validation while also indicating that a custom presence validation was specified.
808 |
809 | ### Before Validations
810 |
811 | If you combine Rails ActionController's `before_filter` and ActiveModel's `validates` then you have approximately what `ActionLogic` defines as `validates_before`.
812 | Before validations can be defined in any of the `ActionLogic` abstractions (`ActionTask`, `ActionUseCase` and `ActionCoordinator`). In each abstraction a `validates_before`
813 | operation is performed *before* invoking the `call` method.
814 |
815 | 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
816 | validation on a single attribute:
817 |
818 | ```ruby
819 | class ActionTaskExample
820 | include ActionLogic::ActionTask
821 |
822 | validates_before :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } }
823 |
824 | def call
825 | end
826 | end
827 |
828 | result = ActionTaskExample.execute(:example_attribute => [1, 2, 3])
829 |
830 | result # => #
831 | ```
832 |
833 | The following example illustrates how to specify a before validation for multiple attributes:
834 |
835 | ```ruby
836 | class ActionTaskExample
837 | include ActionLogic::ActionTask
838 |
839 | validates_before :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } },
840 | :example_attribute2 => { :type => Fixnum }
841 |
842 | def call
843 | end
844 | end
845 |
846 | result = ActionTaskExample.execute(:example_attribute => [1, 2, 3], :example_attribute2 => 1)
847 |
848 | result # => #
849 | ```
850 |
851 | ### After Validations
852 |
853 | If you combine Rails ActionController's `after_filter` and ActiveModel's `validates` then you have approximately what `ActionLogic` defines as `validates_after`.
854 | After validations can be defined in any of the `ActionLogic` abstractions (`ActionTask`, `ActionUseCase` and `ActionCoordinator`). In each abstraction a `validates_after`
855 | operation is performed *after* invoking the `call` method.
856 |
857 | 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
858 | validation on a single attribute:
859 |
860 | ```ruby
861 | class ActionTaskExample
862 | include ActionLogic::ActionTask
863 |
864 | validates_after :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } }
865 |
866 | def call
867 | context.example_attribute = [1, 2, 3]
868 | end
869 | end
870 |
871 | result = ActionTaskExample.execute
872 |
873 | result # => #
874 | ```
875 | The following example illustrates how to specify an after validation for multiple attributes:
876 |
877 | ```ruby
878 | class ActionTaskExample
879 | include ActionLogic::ActionTask
880 |
881 | validates_after :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } },
882 | :example_attribute2 => { :type => Fixnum }
883 |
884 | def call
885 | context.example_attribute = [1, 2, 3]
886 | context.example_attribute2 = 1
887 | end
888 | end
889 |
890 | result = ActionTaskExample.execute
891 |
892 | result # => #
893 | ```
894 |
895 | ### Around Validations
896 |
897 | If you combine Rails ActionController's `around_filter` and ActiveModel's `validates` then you have approximately what `ActionLogic` defines as `validates_around`.
898 | Around validations can be defined in any of the `ActionLogic` abstractions (`ActionTask`, `ActionUseCase` and `ActionCoordinator`). In each abstraction a `validates_around`
899 | operation is performed *before* and *after* invoking the `call` method.
900 |
901 | 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
902 | validation on a single attribute:
903 |
904 | ```ruby
905 | class ActionTaskExample
906 | include ActionLogic::ActionTask
907 |
908 | validates_around :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } }
909 |
910 | def call
911 | end
912 | end
913 |
914 | result = ActionTaskExample.execute(:example_attribute => [1, 2, 3])
915 |
916 | result # => #
917 | ```
918 | The following example illustrates how to specify an around validation for multiple attributes:
919 |
920 | ```ruby
921 | class ActionTaskExample
922 | include ActionLogic::ActionTask
923 |
924 | validates_around :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } },
925 | :example_attribute2 => { :type => Fixnum }
926 |
927 | def call
928 | end
929 | end
930 |
931 | result = ActionTaskExample.execute(:example_attribute => [1, 2, 3], :example_attribute2 => 1)
932 |
933 | result # => #
934 | ```
935 |
936 | ### Benchmarking
937 |
938 | At some point you may want to benchmark and profile the performance of your code. `ActionLogic` allows for benchmarking that
939 | range from simple defaults to highly customizable options depending on your use case and needs.
940 |
941 | ### Enable Benchmarking
942 |
943 | Because benchmarking negatively impacts performance, we must explicitly tell `ActionLogic` that we want to benchmark (otherwise
944 | it defaults to ignore benchmarking). To do this, we configure `ActionLogic` using the `configure` method. With the provided
945 | `config` object, we explicitly enable benchmarking by setting `config.benchmark = true`:
946 |
947 | ```ruby
948 | ActionLogic.configure do |config|
949 | config.benchmark = true
950 | end
951 | ```
952 |
953 | ### Benchmark Logging
954 |
955 | Additionally, `ActionLogic` writes a benchmark log to `$stdout` by default, or you can override this default configuration
956 | by specifying a log file. To do this, you configure `ActionLogic` to use a `File` object for logging benchmark results via the
957 | `ActionLogic.configure` method:
958 |
959 | ```ruby
960 | ActionLogic.configure do |config|
961 | config.benchmark = true
962 | config.benchmark_log = File.open("benchmark.log", "w")
963 | end
964 | ```
965 |
966 | ### Benchmark Log Formatting
967 |
968 | By default, `ActionLogic` formats benchmark logs in the following format:
969 |
970 | ```
971 | context:ValidateAroundPresenceTestUseCase user_time:0.000000 system_time:0.000000 total_time:0.000000 real_time:0.000135
972 | ...
973 | ```
974 |
975 | The default format is intended to be machine readable for easy parsing and is not intended to be used for human reading.
976 | However, if you wish to change the format of the log output, `ActionLogic` allows you to override the default formatter by
977 | allowing you to provide your own formatter:
978 |
979 | ```ruby
980 | ActionLogic.configure do |config|
981 | config.benchmark = true
982 | config.benchmark_log = File.open("benchmark.log", "w")
983 | config.benchmark_formatter = YourCustomFormatter
984 | end
985 | ```
986 |
987 | Where `YourCustomFormatter` subclasses `ActionLogic::ActionBenchmark::DefaultFormatter`:
988 |
989 | ```ruby
990 | class CustomFormatter < ActionLogic::ActionBenchmark::DefaultFormatter
991 |
992 | def log_coordinator(benchmark_result, execution_context_name)
993 | benchmark_log.puts("The ActionCoordinator #{execution_context_name} took #{benchmark_result.real} to complete.")
994 | end
995 |
996 | def log_use_case(benchmark_result, execution_context_name)
997 | benchmark_log.puts("The ActionUseCase #{execution_context_name} took #{benchmark_result.real} to complete.")
998 | end
999 |
1000 | def log_task(benchmark_result, execution_context_name)
1001 | benchmark_log.puts("The ActionTask #{execution_context_name} took #{benchmark_result.real} to complete.")
1002 | end
1003 |
1004 | end
1005 | ```
1006 |
1007 | From the example above, you can see that a custom formatter is required to define three methods: `log_coordinator`, `log_use_case` and `log_task`. The `log_t cqcoordinator`
1008 | method is called when a `ActionCoordinator` context is benchmarked. The `use_case` and `task` methods are invoked when `ActionUseCase` and `ActionTask`
1009 | contexts are benchmarked, respectively.
1010 |
1011 | Each of the three log methods receives two input parameters: `benchmark_result` and `execution_context_name` where `benchmark_result` is a Ruby
1012 | standard library `Benchmark` result object, and `execution_context_name` is the class name of the `ActionLogic` context.
1013 |
1014 | Once configured, you can verify that the formatter outputs to the specified log file by executing your `ActionLogic` contexts
1015 | and verifying that the log file is written to with the correct format:
1016 |
1017 | ```
1018 | The ActionUseCase TestUseCase2 took 0.00011722202179953456 to complete.
1019 | The ActionTask TestTask3 took 4.570698365569115e-05 to complete.
1020 | ...
1021 | ```
1022 |
1023 | ### Custom Benchmark Handling
1024 |
1025 | By default, `ActionLogic` benchmarks execution contexts using Ruby's `Benchmark` module. If you are content with a `Benchmark` result object, then
1026 | you do not need to specify a custom benchmark handler. However, if you wish to have maximum control, or you require something different than Ruby's
1027 | `Benchmark` module, you can define a custom handler like so:
1028 |
1029 | ```ruby
1030 | class CustomHandler
1031 | def call
1032 | # custom logic
1033 | yield
1034 | # custom logic
1035 | end
1036 | end
1037 | ```
1038 |
1039 | Your custom handler is free to define any custom logic, but you must yield during the body of the `call` method. This is what triggers the execution
1040 | context and will allow your custom handler to measure the length of execution. If you do not yield, the relevant `ActionCoordinator`, `ActionUseCase`
1041 | or `ActionTask` will not be executed and will result in no execution to benchmark.
1042 |
1043 | Additionally, you must register your custom handler with `ActionLogic` using `ActionLogic.configure`:
1044 |
1045 | ```ruby
1046 | ActionLogic.configure do |config|
1047 | config.benchmark = true
1048 | config.benchmark_log = File.open("benchmark.log", "w")
1049 | config.benchmark_handler = CustomHandler.new
1050 | end
1051 | ```
1052 |
1053 | ### Installation
1054 |
1055 | Add `ActionLogic` to your project's Gemfile:
1056 |
1057 | `gem 'action_logic'`
1058 |
1059 | Don't forget to bundle:
1060 |
1061 | `$ bundle`
1062 |
1063 | ### Contributing
1064 |
1065 | Interested in contributing to `ActionLogic`? If so that is awesome! <3
1066 | Please see the [contributing doc](https://github.com/rewinfrey/ActionLogic/blob/master/CONTRIBUTING.md) for details.
1067 |
--------------------------------------------------------------------------------
/action_logic.gemspec:
--------------------------------------------------------------------------------
1 | require File.expand_path('../lib/action_logic/version', __FILE__)
2 |
3 | Gem::Specification.new do |s|
4 | s.name = 'action_logic'
5 | s.summary = 'Business logic abstraction'
6 | s.homepage = 'https://github.com/rewinfrey/action_logic'
7 | s.license = 'MIT'
8 |
9 | s.files = `git ls-files`.split($\) - ['resources']
10 | s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
11 | s.test_files = s.files.grep(%r{^(test|spec|features)/})
12 | s.require_paths = ["lib"]
13 | s.version = ActionLogic::VERSION
14 |
15 | s.authors = ["Rick Winfrey"]
16 | s.email = 'rick.winfrey@gmail.com'
17 | s.date = '2018-12-17'
18 | s.description = 'Provides common interfaces for validating and abstracting business logic'
19 |
20 | s.add_development_dependency("rspec", "~> 3.11")
21 | s.add_development_dependency("pry", "~> 0.14.1")
22 | s.add_development_dependency("rake", "~> 13.0.6")
23 | s.add_development_dependency("simplecov", "~> 0.21.2")
24 | s.add_development_dependency("codecov", "~> 0.6.0")
25 | end
26 |
--------------------------------------------------------------------------------
/lib/action_logic.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/action_context'
2 | require 'action_logic/action_coordinator'
3 | require 'action_logic/action_core'
4 | require 'action_logic/action_task'
5 | require 'action_logic/action_use_case'
6 | require 'action_logic/action_validation'
7 | require 'action_logic/action_benchmark'
8 | require 'action_logic/action_benchmark/default_formatter'
9 | require 'action_logic/action_benchmark/default_benchmark_block'
10 |
11 | require 'action_logic/configuration'
12 | require 'action_logic/errors'
13 | require 'action_logic/version'
14 |
--------------------------------------------------------------------------------
/lib/action_logic/action_benchmark.rb:
--------------------------------------------------------------------------------
1 | module ActionLogic
2 | module ActionBenchmark
3 | module ClassMethods
4 | def with_benchmark(execution_context, &block)
5 | ActionLogic.benchmark? ? benchmark!(execution_context, &block) : block.call
6 | end
7 |
8 | private
9 |
10 | def benchmark!(execution_context, &block)
11 | context = nil
12 | benchmark_result = ActionLogic.benchmark_handler.call { context = block.call }
13 | log!(benchmark_result, execution_context)
14 | context
15 | end
16 |
17 | def log!(benchmark_result, execution_context)
18 | ActionLogic.benchmark_formatter.send("log_#{execution_context.__private__type}".to_sym,
19 | benchmark_result,
20 | execution_context.name)
21 | end
22 |
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/action_logic/action_benchmark/default_benchmark_block.rb:
--------------------------------------------------------------------------------
1 | require 'benchmark'
2 |
3 | module ActionLogic
4 | module ActionBenchmark
5 | class DefaultBenchmarkHandler
6 | def call
7 | Benchmark.measure { yield }
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/action_logic/action_benchmark/default_formatter.rb:
--------------------------------------------------------------------------------
1 | module ActionLogic
2 | module ActionBenchmark
3 | class DefaultFormatter
4 | def initialize(benchmark_log: ActionLogic.benchmark_log)
5 | @benchmark_log = benchmark_log
6 | end
7 |
8 | def format(benchmark_result, context_name)
9 | benchmark_log.printf("%s%s %s%f %s%f %s%f %s%f\n",
10 | "context:",
11 | context_name,
12 | "user_time:",
13 | benchmark_result.utime,
14 | "system_time:",
15 | benchmark_result.stime,
16 | "total_time:",
17 | benchmark_result.total,
18 | "real_time:",
19 | benchmark_result.real)
20 | end
21 |
22 | alias_method :log_coordinator, :format
23 | alias_method :log_use_case, :format
24 | alias_method :log_task, :format
25 |
26 | private
27 | attr_reader :benchmark_log
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/action_logic/action_context.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 |
3 | module ActionLogic
4 | class ActionContext < OpenStruct
5 | SUCCESS = :success
6 | FAILURE = :failure
7 | HALTED = :halted
8 |
9 | def initialize(params = {})
10 | params[:status] ||= SUCCESS
11 | super(params)
12 | end
13 |
14 | def update!(status, message)
15 | self.status = status
16 | self.message = message
17 | end
18 |
19 | def fail!(message = "")
20 | update!(FAILURE, message)
21 | end
22 |
23 | def halt!(message = "")
24 | update!(HALTED, message)
25 | end
26 |
27 | def success?
28 | self.status == SUCCESS
29 | end
30 |
31 | def failure?
32 | self.status == FAILURE
33 | end
34 |
35 | def halted?
36 | self.status == HALTED
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/action_logic/action_coordinator.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/action_includes'
2 |
3 | module ActionLogic
4 | module ActionCoordinator
5 |
6 | def self.included(klass)
7 | klass.extend ActionLogic::ActionIncludes
8 | klass.extend ClassMethods
9 | end
10 |
11 | module ClassMethods
12 | def execute(params = {})
13 | around(params) do |execution_context|
14 | execution_context.call
15 |
16 | next_execution_context = execution_context.plan.keys.first
17 |
18 | while (next_execution_context) do
19 | execution_context.context = next_execution_context.execute(execution_context.context)
20 | next_execution_context = execution_context.plan[next_execution_context][execution_context.context.status]
21 |
22 | # From the perspective of the coordinator, the status of the context should be
23 | # :success as long as the state transition plan defines the next execution context
24 | # for a given current exection context and its resulting context state.
25 | # However, because normally a context in a state of :halted or :failure would
26 | # be considered a "breaking" state, the status of a context that is :halted or :failure
27 | # has to be reset to the default :success status only within the execution context of
28 | # the coordinator and only when the next execution context is defined within the
29 | # state transition plan. Otherwise, the context is return as is, without mutating its :status.
30 | execution_context.context.status = :success if next_execution_context
31 | end
32 |
33 | execution_context.context
34 | end
35 | end
36 |
37 | def __private__type
38 | "coordinator"
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/action_logic/action_core.rb:
--------------------------------------------------------------------------------
1 | module ActionLogic
2 | module ActionCore
3 | attr_accessor :context
4 |
5 | def initialize(params)
6 | self.context = make_context(params)
7 | end
8 |
9 | def make_context(params = {})
10 | ActionContext.new(params)
11 | end
12 |
13 | def break?
14 | context.status == :failure ||
15 | context.status == :halted
16 | end
17 |
18 | module ClassMethods
19 | def around(params, &block)
20 | with_benchmark(self) do
21 | execute!(params, &block)
22 | end
23 | end
24 |
25 | def execute!(params, &block)
26 | execution_context = self.new(params)
27 |
28 | return execution_context.context if execution_context.break?
29 |
30 | execution_context.set_validation_rules
31 | execution_context.validations!(:before)
32 | execution_context.validations!(:around)
33 |
34 | begin
35 | block.call(execution_context)
36 | rescue => e
37 | if execution_context.respond_to?(:error)
38 | execution_context.error(e)
39 | else
40 | raise e
41 | end
42 | end
43 |
44 | execution_context.validations!(:after)
45 | execution_context.validations!(:around)
46 |
47 | execution_context.context
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/action_logic/action_includes.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/action_core'
2 | require 'action_logic/action_validation'
3 | require 'action_logic/action_benchmark'
4 |
5 | module ActionLogic
6 | module ActionIncludes
7 | def self.extended(klass)
8 | klass.include ActionLogic::ActionCore
9 | klass.include ActionLogic::ActionValidation
10 | klass.extend ActionLogic::ActionCore::ClassMethods
11 | klass.extend ActionLogic::ActionValidation::ClassMethods
12 | klass.extend ActionLogic::ActionBenchmark::ClassMethods
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/action_logic/action_task.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/action_includes'
2 |
3 | module ActionLogic
4 | module ActionTask
5 |
6 | def self.included(klass)
7 | klass.extend ActionLogic::ActionIncludes
8 | klass.extend ClassMethods
9 | end
10 |
11 | module ClassMethods
12 | def execute(params = {})
13 | around(params) do |execution_context|
14 | execution_context.call
15 | execution_context.context
16 | end
17 | end
18 |
19 | def __private__type
20 | "task"
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/action_logic/action_use_case.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/action_includes'
2 |
3 | module ActionLogic
4 | module ActionUseCase
5 |
6 | def self.included(klass)
7 | klass.extend ActionLogic::ActionIncludes
8 | klass.extend ClassMethods
9 | end
10 |
11 | module ClassMethods
12 | def execute(params = {})
13 | around(params) do |execution_context|
14 | raise ActionLogic::InvalidUseCaseError.new("ActionUseCase requires at least one ActionTask") if execution_context.tasks.empty?
15 |
16 | execution_context.call
17 |
18 | execution_context.tasks.reduce(execution_context.context) do |context, task|
19 | execution_context.context = task.execute(context)
20 | execution_context.context
21 | end
22 | end
23 | end
24 |
25 | def __private__type
26 | "use_case"
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/action_logic/action_validation.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/action_validation/attribute_validation'
2 | require 'action_logic/action_validation/presence_validation'
3 | require 'action_logic/action_validation/type_validation'
4 |
5 | module ActionLogic
6 | module ActionValidation
7 | module ClassMethods
8 | def validates_before(args)
9 | @validates_before = args
10 | end
11 |
12 | def validates_after(args)
13 | @validates_after = args
14 | end
15 |
16 | def validates_around(args)
17 | @validates_around = args
18 | end
19 |
20 | def get_validates_before
21 | @validates_before ||= {}
22 | end
23 |
24 | def get_validates_after
25 | @validates_after ||= {}
26 | end
27 |
28 | def get_validates_around
29 | @validates_around ||= {}
30 | end
31 | end
32 |
33 | def validations
34 | [AttributeValidation,
35 | TypeValidation,
36 | PresenceValidation]
37 | end
38 |
39 | def validate!(validation, validation_rules)
40 | return if validation_rules.empty?
41 | validation.validate!(validation_rules, context)
42 | end
43 |
44 | def validations!(validation_order)
45 | case validation_order
46 | when :before then validations.each { |validation| validate!(validation, @before_validation_rules) }
47 | when :after then validations.each { |validation| validate!(validation, @after_validation_rules) }
48 | when :around then validations.each { |validation| validate!(validation, @around_validation_rules) }
49 | end
50 | end
51 |
52 | def set_validation_rules
53 | @before_validation_rules ||= self.class.get_validates_before
54 | @after_validation_rules ||= self.class.get_validates_after
55 | @around_validation_rules ||= self.class.get_validates_around
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/action_logic/action_validation/attribute_validation.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/errors'
2 | require 'action_logic/action_validation/base_validation'
3 |
4 | module ActionLogic
5 | module ActionValidation
6 | class AttributeValidation < BaseValidation
7 |
8 | def self.validate!(validation_rules, context)
9 | existing_attributes = context.to_h.keys
10 | expected_attributes = validation_rules.keys || []
11 | missing_attributes = expected_attributes - existing_attributes
12 |
13 | raise ActionLogic::MissingAttributeError.new(error_message_format(missing_attributes.join(", ") + " attributes are missing")) if missing_attributes.any?
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/action_logic/action_validation/base_validation.rb:
--------------------------------------------------------------------------------
1 | module ActionLogic
2 | module ActionValidation
3 | class BaseValidation
4 | def self.error_message_format(error_string)
5 | "context: #{self.class} message: #{error_string}"
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/action_logic/action_validation/presence_validation.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/errors'
2 | require 'action_logic/action_validation/base_validation'
3 |
4 | module ActionLogic
5 | module ActionValidation
6 | class PresenceValidation < BaseValidation
7 |
8 | def self.validate!(validation_rules, context)
9 | return unless validation_rules.values.find { |expected_validation| expected_validation[:presence] }
10 | errors = presence_errors(validation_rules, context)
11 | raise ActionLogic::PresenceError.new(errors) if errors.any?
12 | end
13 |
14 | def self.presence_errors(validation_rules, context)
15 | validation_rules.reduce([]) do |error_collection, (expected_attribute, expected_validation)|
16 | next error_collection unless expected_validation[:presence]
17 | error_collection << error_message(expected_attribute, expected_validation, context)
18 | error_collection
19 | end || []
20 | end
21 |
22 | def self.error_message(expected_attribute, expected_validation, context)
23 | case expected_validation[:presence]
24 | when TrueClass then "Attribute: #{expected_attribute} is missing value in context but presence validation was specified" unless context[expected_attribute]
25 | when Proc then "Attribute: #{expected_attribute} is missing value in context but custom presence validation was specified" unless expected_validation[:presence].call(context[expected_attribute])
26 | else
27 | raise ActionLogic::UnrecognizablePresenceValidatorError.new(error_message_format("Presence validator: #{expected_validation[:presence]} is not a supported format"))
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/action_logic/action_validation/type_validation.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic/errors'
2 | require 'action_logic/action_validation/base_validation'
3 |
4 | module ActionLogic
5 | module ActionValidation
6 | class TypeValidation < BaseValidation
7 |
8 | def self.validate!(validation_rules, context)
9 | return unless validation_rules.values.find { |expected_validation| expected_validation[:type] }
10 |
11 | type_errors = validation_rules.reduce([]) do |collection, (expected_attribute, expected_validation)|
12 | next collection unless expected_validation[:type]
13 |
14 | if context.to_h[expected_attribute].class != expected_validation[:type]
15 | collection << "Attribute: #{expected_attribute} with value: #{context.to_h[expected_attribute]} was expected to be of type #{expected_validation[:type]} but is #{context.to_h[expected_attribute].class}"
16 | end
17 | collection
18 | end
19 |
20 | raise ActionLogic::AttributeTypeError.new(error_message_format(type_errors.join(", "))) if type_errors.any?
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/action_logic/configuration.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 |
3 | module ActionLogic
4 | extend self
5 |
6 | def self.configure(&block)
7 | block.call(configuration_options)
8 | end
9 |
10 | def self.configuration_options
11 | @configuration_options ||= OpenStruct.new
12 | end
13 |
14 | def self.benchmark?
15 | configuration_options.benchmark || false
16 | end
17 |
18 | def self.benchmark_log
19 | configuration_options.benchmark_log || $stdout
20 | end
21 |
22 | def self.benchmark_formatter
23 | custom_benchmark_formatter || default_formatter
24 | end
25 |
26 | def self.benchmark_handler
27 | configuration_options.benchmark_handler || ActionBenchmark::DefaultBenchmarkHandler.new
28 | end
29 |
30 | def self.reset!
31 | @configuration_options = OpenStruct.new
32 | @custom_benchmark_formatter = nil
33 | @default_formatter = nil
34 | end
35 |
36 | def self.custom_benchmark_formatter
37 | @custom_benchmark_formatter ||= configuration_options.benchmark_formatter &&
38 | configuration_options.benchmark_formatter.new
39 | end
40 |
41 | def self.default_formatter
42 | @default_formatter ||= ActionBenchmark::DefaultFormatter.new
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/action_logic/errors.rb:
--------------------------------------------------------------------------------
1 | module ActionLogic
2 | # Thrown whenever an ActionTask, ActionUseCase or ActionCoordinator's context does not have a key defined for the attribute key in a validations block
3 | class MissingAttributeError < StandardError; end
4 |
5 | # Thrown whenever an ActionTask, ActionUseCase or ActionCoordinator's context has an attribute and value but the value's type is not the same as that
6 | # attributey's type specified in a validations block
7 | class AttributeTypeError < StandardError; end
8 |
9 | # Thrown whenever an ActionTask, ActionUseCase or ActionCoordinator's context has an attribute and value but the value definition of presence is not satisfied
10 | # for the value stored on the context
11 | class PresenceError < StandardError; end
12 |
13 | # Adding a custom presence definition is possible, but the presence validation will throw an error if the custom presence definition is not a Proc
14 | class UnrecognizablePresenceValidatorError < StandardError; end
15 |
16 | # ActionUseCases are invalid if they do not define any tasks
17 | class InvalidUseCaseError < StandardError; end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/action_logic/version.rb:
--------------------------------------------------------------------------------
1 | module ActionLogic
2 | VERSION = '0.3.3'
3 | end
4 |
--------------------------------------------------------------------------------
/resources/action_coordinator_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rewinfrey/ActionLogic/9341e76b03a9500031dd6fa8154ccbfd2dec31ee/resources/action_coordinator_diagram.png
--------------------------------------------------------------------------------
/resources/action_task_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rewinfrey/ActionLogic/9341e76b03a9500031dd6fa8154ccbfd2dec31ee/resources/action_task_diagram.png
--------------------------------------------------------------------------------
/resources/action_use_case_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rewinfrey/ActionLogic/9341e76b03a9500031dd6fa8154ccbfd2dec31ee/resources/action_use_case_diagram.png
--------------------------------------------------------------------------------
/resources/diagrams.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rewinfrey/ActionLogic/9341e76b03a9500031dd6fa8154ccbfd2dec31ee/resources/diagrams.sketch
--------------------------------------------------------------------------------
/resources/overview_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rewinfrey/ActionLogic/9341e76b03a9500031dd6fa8154ccbfd2dec31ee/resources/overview_diagram.png
--------------------------------------------------------------------------------
/spec/action_logic/action_benchmark/default_formatter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module ActionLogic::ActionBenchmark
4 | describe DefaultFormatter do
5 |
6 | let(:benchmark_log) { StringIO.new }
7 | let(:benchmark_result) { double(:benchmark_result, utime: 0.00003, stime: 0.00002, total: 0.00001, real: 0.00030) }
8 |
9 | subject { described_class.new(benchmark_log: benchmark_log) }
10 |
11 | it "writes the benchmark result to the log for an ActionCoordinator" do
12 | subject.log_coordinator(benchmark_result, "CoordinatorContext")
13 | expect(benchmark_log.string).to\
14 | eq "context:CoordinatorContext user_time:0.000030 system_time:0.000020 total_time:0.000010 real_time:0.000300\n"
15 | end
16 |
17 | it "writes the benchmark result to the log for an ActionUseCase" do
18 | subject.log_use_case(benchmark_result, "UseCaseContext")
19 | expect(benchmark_log.string).to\
20 | eq "context:UseCaseContext user_time:0.000030 system_time:0.000020 total_time:0.000010 real_time:0.000300\n"
21 | end
22 |
23 | it "writes the benchmark result to the log for an ActionTask" do
24 | subject.log_task(benchmark_result, "TaskContext")
25 | expect(benchmark_log.string).to\
26 | eq "context:TaskContext user_time:0.000030 system_time:0.000020 total_time:0.000010 real_time:0.000300\n"
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/action_logic/action_context_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'action_logic'
3 | require 'fixtures/constants'
4 |
5 | module ActionLogic
6 | describe ActionContext do
7 | subject { ActionContext.new }
8 |
9 | describe "initialization" do
10 | it "sets a default success attribute on the context" do
11 | expect(subject.status).to eq(described_class::SUCCESS)
12 | end
13 | end
14 |
15 | describe "success?" do
16 | it "returns true if the context is successful" do
17 | expect(subject.success?).to be_truthy
18 | end
19 | end
20 |
21 | describe "failing a context" do
22 | it "sets the context status as failed" do
23 | subject.fail!
24 |
25 | expect(subject.status).to eq(:failure)
26 | end
27 |
28 | it "does not require a message" do
29 | subject.fail!
30 |
31 | expect(subject.message).to be_empty
32 | end
33 |
34 | it "allows a custom failure message to be defined" do
35 | failure_message = Constants::FAILURE_MESSAGE
36 | subject.fail!(failure_message)
37 |
38 | expect(subject.message).to eq(failure_message)
39 | end
40 |
41 | it "responds to directly query" do
42 | subject.fail!
43 |
44 | expect(subject.failure?).to be_truthy
45 | end
46 | end
47 |
48 | describe "halting a context" do
49 | it "sets the context status as halted" do
50 | subject.halt!
51 |
52 | expect(subject.status).to eq(:halted)
53 | end
54 |
55 | it "does not require a message" do
56 | subject.halt!
57 |
58 | expect(subject.message).to be_empty
59 | end
60 |
61 | it "allows a custom halted message to be defined" do
62 | halt_message = Constants::HALT_MESSAGE
63 | subject.halt!(halt_message)
64 |
65 | expect(subject.message).to eq(halt_message)
66 | end
67 |
68 | it "responds to direct query" do
69 | subject.halt!
70 |
71 | expect(subject.halted?).to be_truthy
72 | end
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/spec/action_logic/action_coordinator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'action_logic'
3 | require 'fixtures/coordinators'
4 | require 'fixtures/custom_types'
5 |
6 | module ActionLogic
7 | describe ActionCoordinator do
8 | it "knows its type" do
9 | expect(TestCoordinator1.__private__type).to eq("coordinator")
10 | end
11 |
12 | context "no failures and no halts" do
13 | it "evaluates all use cases defined by the state transition plan" do
14 | result = TestCoordinator1.execute()
15 |
16 | expect(result.test_coordinator1).to be_truthy
17 | expect(result.test_use_case1).to be_truthy
18 | expect(result.test_task1).to be_truthy
19 | expect(result.test_use_case2).to be_truthy
20 | expect(result.test_task2).to be_truthy
21 | expect(result.test_use_case3).to be_truthy
22 | expect(result.test_task3).to be_truthy
23 | end
24 | end
25 |
26 | context "with halts" do
27 | it "evaluates all use cases defined by the state transition plan" do
28 | result = HaltedTestCoordinator1.execute()
29 |
30 | expect(result.halted_test_coordinator1).to be_truthy
31 | expect(result.halted_test_use_case1).to be_truthy
32 | expect(result.halted_test_task1).to be_truthy
33 | expect(result.test_use_case2).to be_truthy
34 | expect(result.test_task2).to be_truthy
35 | expect(result.test_use_case3).to be_truthy
36 | expect(result.test_task3).to be_truthy
37 | end
38 | end
39 |
40 | context "with failures" do
41 | it "evaluates all use cases defined by the state transition plan" do
42 | result = FailureTestCoordinator1.execute()
43 |
44 | expect(result.failure_test_coordinator1).to be_truthy
45 | expect(result.failure_test_use_case1).to be_truthy
46 | expect(result.failure_test_task1).to be_truthy
47 | expect(result.test_use_case2).to be_truthy
48 | expect(result.test_task2).to be_truthy
49 | expect(result.test_use_case3).to be_truthy
50 | expect(result.test_task3).to be_truthy
51 | end
52 | end
53 |
54 | describe "before validations" do
55 | describe "required attributes and type validation" do
56 | it "does not raise error if context has required keys and values are of the correct type" do
57 | expect { ValidateBeforeTestCoordinator.execute(Constants::VALID_ATTRIBUTES) }.to_not raise_error
58 | end
59 |
60 | it "raises error if context is missing required keys" do
61 | expect { ValidateBeforeTestCoordinator.execute() }.to\
62 | raise_error(ActionLogic::MissingAttributeError)
63 | end
64 |
65 | it "raises error if context has required key but is not of correct type" do
66 | expect { ValidateBeforeTestCoordinator.execute(Constants::INVALID_ATTRIBUTES) }.to\
67 | raise_error(ActionLogic::AttributeTypeError)
68 | end
69 | end
70 |
71 | describe "custom types" do
72 | it "allows validation against custom defined types" do
73 | expect { ValidateBeforeCustomTypeTestCoordinator.execute(Constants::CUSTOM_TYPE_ATTRIBUTES1) }.to_not raise_error
74 | end
75 |
76 | it "raises error if context has custom type attribute but value is not correct custom type" do
77 | expect { ValidateBeforeCustomTypeTestCoordinator.execute(Constants::CUSTOM_TYPE_ATTRIBUTES2) }.to\
78 | raise_error(ActionLogic::AttributeTypeError)
79 | end
80 | end
81 |
82 | describe "presence" do
83 | it "validates presence if presence is specified" do
84 | expect { ValidateBeforePresenceTestCoordinator.execute(:integer_test => 1) }.to_not raise_error
85 | end
86 |
87 | it "raises error if context has required key but value is not defined when validation requires presence" do
88 | expect { ValidateBeforePresenceTestCoordinator.execute(:integer_test => nil) }.to\
89 | raise_error(ActionLogic::PresenceError)
90 | end
91 | end
92 |
93 | describe "custom presence" do
94 | it "allows custom presence validation if custom presence is defined" do
95 | expect { ValidateBeforeCustomPresenceTestCoordinator.execute(:array_test => [1]) }.to_not raise_error
96 | end
97 |
98 | it "raises error if custom presence validation is not satisfied" do
99 | expect { ValidateBeforeCustomPresenceTestCoordinator.execute(:array_test => []) }.to\
100 | raise_error(ActionLogic::PresenceError)
101 | end
102 |
103 | it "raises error if custom presence validation is not supported" do
104 | expect { ValidateBeforeUnrecognizablePresenceTestCoordinator.execute(:integer_test => 1) }.to\
105 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
106 | end
107 | end
108 | end
109 |
110 | describe "after validations" do
111 | describe "required attributes and type validation" do
112 | it "does not raise error if the task sets all required keys and values are of the correct type" do
113 | expect { ValidateAfterTestCoordinator.execute() }.to_not raise_error
114 | end
115 |
116 | it "raises error if task does not provide the necessary keys" do
117 | expect { ValidateAfterMissingAttributesTestCoordinator.execute() }.to\
118 | raise_error(ActionLogic::MissingAttributeError)
119 | end
120 |
121 | it "raises error if task has required key but is not of correct type" do
122 | expect { ValidateAfterInvalidTypeTestCoordinator.execute() }.to\
123 | raise_error(ActionLogic::AttributeTypeError)
124 | end
125 | end
126 |
127 | describe "custom types" do
128 | it "allows validation against custom defined types" do
129 | expect { ValidateAfterCustomTypeTestCoordinator.execute() }.to_not raise_error
130 | end
131 |
132 | it "raises error if context has custom type attribute but value is not correct custom type" do
133 | expect { ValidateAfterInvalidCustomTypeTestCoordinator.execute() }.to\
134 | raise_error(ActionLogic::AttributeTypeError)
135 | end
136 | end
137 |
138 | describe "presence" do
139 | it "validates presence if presence is specified" do
140 | expect { ValidateAfterPresenceTestCoordinator.execute() }.to_not raise_error
141 | end
142 |
143 | it "raises error if context has required key but value is not defined when validation requires presence" do
144 | expect { ValidateAfterInvalidPresenceTestCoordinator.execute() }.to\
145 | raise_error(ActionLogic::PresenceError)
146 | end
147 | end
148 |
149 | describe "custom presence" do
150 | it "allows custom presence validation if custom presence is defined" do
151 | expect { ValidateAfterCustomPresenceTestCoordinator.execute() }.to_not raise_error
152 | end
153 |
154 | it "raises error if custom presence validation is not satisfied" do
155 | expect { ValidateAfterInvalidCustomPresenceTestCoordinator.execute() }.to\
156 | raise_error(ActionLogic::PresenceError)
157 | end
158 |
159 | it "raises error if custom presence validation is not supported" do
160 | expect { ValidateAfterUnrecognizablePresenceTestCoordinator.execute() }.to\
161 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
162 | end
163 | end
164 | end
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/spec/action_logic/action_task_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'action_logic'
3 | require 'fixtures/tasks'
4 |
5 | module ActionLogic
6 | describe ActionTask do
7 | it "knows its type" do
8 | expect(SimpleTestTask.__private__type).to eq("task")
9 | end
10 |
11 | it "returns an instance of ActionContext" do
12 | result = SimpleTestTask.execute()
13 |
14 | expect(result).to be_a(ActionLogic::ActionContext)
15 | end
16 |
17 | it "sets an attribute and value on the context" do
18 | result = SimpleTestTask.execute()
19 |
20 | expect(result.new_attribute).to be_truthy
21 | end
22 |
23 | describe "around validations" do
24 | describe "required attributes and type validation" do
25 | it "does not raise error if context has required keys and values are of the correct type" do
26 | expect { ValidateAroundTestTask.execute(Constants::VALID_ATTRIBUTES) }.to_not raise_error
27 | end
28 |
29 | it "raises error if context is missing required keys" do
30 | expect { ValidateAroundTestTask.execute() }.to\
31 | raise_error(ActionLogic::MissingAttributeError)
32 | end
33 |
34 | it "raises error if context has required keys but values are not of correct type" do
35 | expect { ValidateAroundTestTask.execute(Constants::INVALID_ATTRIBUTES) }.to\
36 | raise_error(ActionLogic::AttributeTypeError)
37 | end
38 | end
39 |
40 | describe "custom types" do
41 | it "allows validation against custom defined types" do
42 | expect { ValidateAroundCustomTypeTestTask.execute(:custom_type => CustomType1.new) }.to_not raise_error
43 | end
44 |
45 | it "raises error if context has custom type attribute but value is not correct custom type" do
46 | expect { ValidateAroundCustomTypeTestTask.execute(:custom_type => CustomType2.new) }.to\
47 | raise_error(ActionLogic::AttributeTypeError)
48 | end
49 | end
50 |
51 | describe "presence" do
52 | it "validates presence if presence is specified" do
53 | expect { ValidateAroundPresenceTestTask.execute(:integer_test => 1) }.to_not raise_error
54 | end
55 |
56 | it "raises error if context has required key but value is not defined when validation requires presence" do
57 | expect { ValidateAroundPresenceTestTask.execute(:integer_test => nil) }.to\
58 | raise_error(ActionLogic::PresenceError)
59 | end
60 | end
61 |
62 | describe "custom presence" do
63 | it "allows custom presence validation if custom presence is defined" do
64 | expect { ValidateAroundCustomPresenceTestTask.execute(:array_test => [1]) }.to_not raise_error
65 | end
66 |
67 | it "raises error if custom presence validation is not satisfied" do
68 | expect { ValidateAroundCustomPresenceTestTask.execute(:array_test => []) }.to\
69 | raise_error(ActionLogic::PresenceError)
70 | end
71 |
72 | it "raises error if custom presence validation is not supported" do
73 | expect { ValidateAroundUnrecognizablePresenceTestTask.execute(:integer_test => 1) }.to\
74 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
75 | end
76 | end
77 | end
78 |
79 | describe "before validations" do
80 | describe "required attributes and type validation" do
81 | it "does not raise error if context has required keys and values are of the correct type" do
82 | expect { ValidateBeforeTestTask.execute(Constants::VALID_ATTRIBUTES) }.to_not raise_error
83 | end
84 |
85 | it "raises error if context is missing required keys" do
86 | expect { ValidateBeforeTestTask.execute() }.to\
87 | raise_error(ActionLogic::MissingAttributeError)
88 | end
89 |
90 | it "raises error if context has required keys but values are not of correct type" do
91 | expect { ValidateBeforeTestTask.execute(Constants::INVALID_ATTRIBUTES) }.to\
92 | raise_error(ActionLogic::AttributeTypeError)
93 | end
94 | end
95 |
96 | describe "custom types" do
97 | it "allows validation against custom defined types" do
98 | expect { ValidateBeforeCustomTypeTestTask.execute(:custom_type => CustomType1.new) }.to_not raise_error
99 | end
100 |
101 | it "raises error if context has custom type attribute but value is not correct custom type" do
102 | expect { ValidateBeforeCustomTypeTestTask.execute(:custom_type => CustomType2.new) }.to\
103 | raise_error(ActionLogic::AttributeTypeError)
104 | end
105 | end
106 |
107 | describe "presence" do
108 | it "validates presence if presence is specified" do
109 | expect { ValidateBeforePresenceTestTask.execute(:integer_test => 1) }.to_not raise_error
110 | end
111 |
112 | it "raises error if context has required key but value is not defined when validation requires presence" do
113 | expect { ValidateBeforePresenceTestTask.execute(:integer_test => nil) }.to\
114 | raise_error(ActionLogic::PresenceError)
115 | end
116 | end
117 |
118 | describe "custom presence" do
119 | it "allows custom presence validation if custom presence is defined" do
120 | expect { ValidateBeforeCustomPresenceTestTask.execute(:array_test => [1]) }.to_not raise_error
121 | end
122 |
123 | it "raises error if custom presence validation is not satisfied" do
124 | expect { ValidateBeforeCustomPresenceTestTask.execute(:array_test => []) }.to\
125 | raise_error(ActionLogic::PresenceError)
126 | end
127 |
128 | it "raises error if custom presence validation is not supported" do
129 | expect { ValidateBeforeUnrecognizablePresenceTestTask.execute(:integer_test => 1) }.to\
130 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
131 | end
132 | end
133 | end
134 |
135 | describe "after validations" do
136 | describe "required attributes and type validation" do
137 | it "does not raise error if the task sets all required keys and values are of the correct type" do
138 | expect { ValidateAfterTestTask.execute() }.to_not raise_error
139 | end
140 |
141 | it "raises error if task does not provide the necessary keys" do
142 | expect { ValidateAfterMissingAttributesTestTask.execute() }.to\
143 | raise_error(ActionLogic::MissingAttributeError)
144 | end
145 |
146 | it "raises error if task has required key but is not of correct type" do
147 | expect { ValidateAfterInvalidTypeTestTask.execute() }.to\
148 | raise_error(ActionLogic::AttributeTypeError)
149 | end
150 | end
151 |
152 | describe "custom types" do
153 | it "allows validation against custom defined types" do
154 | expect { ValidateAfterCustomTypeTestTask.execute() }.to_not raise_error
155 | end
156 |
157 | it "raises error if context has custom type attribute but value is not correct custom type" do
158 | expect { ValidateAfterInvalidCustomTypeTestTask.execute() }.to\
159 | raise_error(ActionLogic::AttributeTypeError)
160 | end
161 | end
162 |
163 | describe "presence" do
164 | it "validates presence if presence is specified" do
165 | expect { ValidateAfterPresenceTestTask.execute() }.to_not raise_error
166 | end
167 |
168 | it "raises error if context has required key but value is not defined when validation requires presence" do
169 | expect { ValidateAfterInvalidPresenceTestTask.execute() }.to\
170 | raise_error(ActionLogic::PresenceError)
171 | end
172 | end
173 |
174 | describe "custom presence" do
175 | it "allows custom presence validation if custom presence is defined" do
176 | expect { ValidateAfterCustomPresenceTestTask.execute() }.to_not raise_error
177 | end
178 |
179 | it "raises error if custom presence validation is not satisfied" do
180 | expect { ValidateAfterInvalidCustomPresenceTestTask.execute() }.to\
181 | raise_error(ActionLogic::PresenceError)
182 | end
183 |
184 | it "raises error if custom presence validation is not supported" do
185 | expect { ValidateAfterUnrecognizablePresenceTestTask.execute() }.to\
186 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
187 | end
188 | end
189 | end
190 |
191 | describe "error handler" do
192 | context "with error handler defined" do
193 | it "does not catch exceptions due to before validation errors" do
194 | expect { ErrorHandlerInvalidAttributesBeforeTestTask.execute() }.to\
195 | raise_error(ActionLogic::MissingAttributeError)
196 | end
197 |
198 | it "does not catch exceptions due to after validation errors" do
199 | expect { ErrorHandlerInvalidAttributesAfterTestTask.execute() }.to\
200 | raise_error(ActionLogic::MissingAttributeError)
201 | end
202 |
203 | it "the error and context are passed to the error handler" do
204 | result = ErrorHandlerTestTask.execute()
205 |
206 | expect(result.e).to be_a(RuntimeError)
207 | expect(result).to be_a(ActionLogic::ActionContext)
208 | end
209 | end
210 |
211 | context "without error handler defined" do
212 | it "raises original exception if error handler is not defined" do
213 | expect { MissingErrorHandlerTestTask.execute() }.to\
214 | raise_error(RuntimeError)
215 | end
216 | end
217 | end
218 |
219 | describe "fail!" do
220 | it "returns the context with the correct status and failure message" do
221 | result = FailureTestTask.execute()
222 |
223 | expect(result.status).to eq(:failure)
224 | expect(result.message).to eq(Constants::FAILURE_MESSAGE)
225 | end
226 | end
227 |
228 | describe "halt!" do
229 | it "returns the context with the correct status and halt message" do
230 | result = HaltTestTask.execute()
231 |
232 | expect(result.status).to eq(:halted)
233 | expect(result.message).to eq(Constants::HALT_MESSAGE)
234 | end
235 | end
236 | end
237 | end
238 |
--------------------------------------------------------------------------------
/spec/action_logic/action_use_case_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'action_logic'
3 | require 'fixtures/use_cases'
4 | require 'fixtures/custom_types'
5 |
6 | module ActionLogic
7 | describe ActionUseCase do
8 | it "knows its type" do
9 | expect(SimpleTestUseCase.__private__type).to eq("use_case")
10 | end
11 |
12 | it "returns an instance of ActionContext" do
13 | result = SimpleTestUseCase.execute()
14 |
15 | expect(result).to be_a(ActionLogic::ActionContext)
16 | end
17 |
18 | it "evalutes a task defined by the use case" do
19 | result = SimpleTestUseCase.execute()
20 |
21 | expect(result.new_attribute).to be_truthy
22 | end
23 |
24 | it "evalutes multiple tasks defined by the use case" do
25 | result = SimpleTestUseCase2.execute()
26 |
27 | expect(result.first).to eq("first")
28 | expect(result.second).to eq("second")
29 | end
30 |
31 | it "calls the use case before evaluating the tasks" do
32 | result = SimpleTestUseCase3.execute()
33 |
34 | expect(result.first).to eq("first")
35 | expect(result.second).to eq("defined in use case")
36 | end
37 |
38 | describe "missing tasks" do
39 | it "raises error if no tasks are defined" do
40 | expect { NoTaskTestUseCase.execute() }.to\
41 | raise_error(ActionLogic::InvalidUseCaseError)
42 | end
43 | end
44 |
45 | describe "around validations" do
46 | describe "required attributes and type validation" do
47 | it "does not raise error if context has required keys and values are of the correct type" do
48 | expect { ValidateAroundTestUseCase.execute(Constants::VALID_ATTRIBUTES) }.to_not raise_error
49 | end
50 |
51 | it "raises error if context is missing required keys" do
52 | expect { ValidateAroundTestUseCase.execute() }.to\
53 | raise_error(ActionLogic::MissingAttributeError)
54 | end
55 |
56 | it "raises error if context has required keys but values are not of correct type" do
57 | expect { ValidateAroundTestUseCase.execute(Constants::INVALID_ATTRIBUTES) }.to\
58 | raise_error(ActionLogic::AttributeTypeError)
59 | end
60 | end
61 |
62 | describe "custom types" do
63 | it "allows validation against custom defined types" do
64 | expect { ValidateAroundCustomTypeTestUseCase.execute(:custom_type => CustomType1.new) }.to_not raise_error
65 | end
66 |
67 | it "raises error if context has custom type attribute but value is not correct custom type" do
68 | expect { ValidateAroundCustomTypeTestUseCase.execute(:custom_type => CustomType2.new) }.to\
69 | raise_error(ActionLogic::AttributeTypeError)
70 | end
71 | end
72 |
73 | describe "presence" do
74 | it "validates presence if presence is specified" do
75 | expect { ValidateAroundPresenceTestUseCase.execute(:integer_test => 1) }.to_not raise_error
76 | end
77 |
78 | it "raises error if context has required key but value is not defined when validation requires presence" do
79 | expect { ValidateAroundPresenceTestUseCase.execute(:integer_test => nil) }.to\
80 | raise_error(ActionLogic::PresenceError)
81 | end
82 | end
83 |
84 | describe "custom presence" do
85 | it "allows custom presence validation if custom presence is defined" do
86 | expect { ValidateAroundCustomPresenceTestUseCase.execute(:array_test => [1]) }.to_not raise_error
87 | end
88 |
89 | it "raises error if custom presence validation is not satisfied" do
90 | expect { ValidateAroundCustomPresenceTestUseCase.execute(:array_test => []) }.to\
91 | raise_error(ActionLogic::PresenceError)
92 | end
93 |
94 | it "raises error if custom presence validation is not supported" do
95 | expect { ValidateAroundUnrecognizablePresenceTestUseCase.execute(:integer_test => 1) }.to\
96 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
97 | end
98 | end
99 | end
100 |
101 | describe "before validations" do
102 | describe "required attributes and type validation" do
103 | it "does not raise error if context has required keys and values are of the correct type" do
104 | expect { ValidateBeforeTestUseCase.execute(Constants::VALID_ATTRIBUTES) }.to_not raise_error
105 | end
106 |
107 | it "raises error if context is missing required keys" do
108 | expect { ValidateBeforeTestUseCase.execute() }.to\
109 | raise_error(ActionLogic::MissingAttributeError)
110 | end
111 |
112 | it "raises error if context has required key but is not of correct type" do
113 | expect { ValidateBeforeTestUseCase.execute(Constants::INVALID_ATTRIBUTES) }.to\
114 | raise_error(ActionLogic::AttributeTypeError)
115 | end
116 | end
117 |
118 | describe "custom types" do
119 | it "allows validation against custom defined types" do
120 | expect { ValidateBeforeCustomTypeTestUseCase.execute(Constants::CUSTOM_TYPE_ATTRIBUTES1) }.to_not raise_error
121 | end
122 |
123 | it "raises error if context has custom type attribute but value is not correct custom type" do
124 | expect { ValidateBeforeCustomTypeTestUseCase.execute(Constants::CUSTOM_TYPE_ATTRIBUTES2) }.to\
125 | raise_error(ActionLogic::AttributeTypeError)
126 | end
127 | end
128 |
129 | describe "presence" do
130 | it "validates presence if presence is specified" do
131 | expect { ValidateBeforePresenceTestUseCase.execute(:integer_test => 1) }.to_not raise_error
132 | end
133 |
134 | it "raises error if context has required key but value is not defined when validation requires presence" do
135 | expect { ValidateBeforePresenceTestUseCase.execute(:integer_test => nil) }.to\
136 | raise_error(ActionLogic::PresenceError)
137 | end
138 | end
139 |
140 | describe "custom presence" do
141 | it "allows custom presence validation if custom presence is defined" do
142 | expect { ValidateBeforeCustomPresenceTestUseCase.execute(:array_test => [1]) }.to_not raise_error
143 | end
144 |
145 | it "raises error if custom presence validation is not satisfied" do
146 | expect { ValidateBeforeCustomPresenceTestUseCase.execute(:array_test => []) }.to\
147 | raise_error(ActionLogic::PresenceError)
148 | end
149 |
150 | it "raises error if custom presence validation is not supported" do
151 | expect { ValidateBeforeUnrecognizablePresenceTestUseCase.execute(:integer_test => 1) }.to\
152 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
153 | end
154 | end
155 |
156 | describe "mixed custom presence and type" do
157 | it "allows custom presence validation to be defined without type if a type validation is defined" do
158 | expect { ValidateBeforeMixedTypeAndPresenceUseCase.execute(odd_integer_test: 1, string_test: "Test") }.to_not raise_error
159 | end
160 |
161 | it "raises error if custom presence validation is not satisfied" do
162 | expect { ValidateBeforeMixedTypeAndPresenceUseCase.execute(odd_integer_test: 2, string_test: "Test") }.to \
163 | raise_error(ActionLogic::PresenceError)
164 | end
165 |
166 | it "raises error if type validation is not satisfied" do
167 | expect { ValidateBeforeMixedTypeAndPresenceUseCase.execute(odd_integer_test: "String", string_test: 15) }.to \
168 | raise_error(ActionLogic::AttributeTypeError)
169 | end
170 |
171 | it "raises error if type presence validation is not satisfied" do
172 | expect { ValidateBeforeMixedTypeAndPresenceUseCase.execute(odd_integer_test: 1) }.to \
173 | raise_error(ActionLogic::MissingAttributeError)
174 | end
175 | end
176 | end
177 |
178 | describe "after validations" do
179 | describe "required attributes and type validation" do
180 | it "does not raise error if the task sets all required keys and values are of the correct type" do
181 | expect { ValidateAfterTestUseCase.execute() }.to_not raise_error
182 | end
183 |
184 | it "raises error if task does not provide the necessary keys" do
185 | expect { ValidateAfterMissingAttributesTestUseCase.execute() }.to\
186 | raise_error(ActionLogic::MissingAttributeError)
187 | end
188 |
189 | it "raises error if task has required key but is not of correct type" do
190 | expect { ValidateAfterInvalidTypeTestUseCase.execute() }.to\
191 | raise_error(ActionLogic::AttributeTypeError)
192 | end
193 | end
194 |
195 | describe "custom types" do
196 | it "allows validation against custom defined types" do
197 | expect { ValidateAfterCustomTypeTestUseCase.execute() }.to_not raise_error
198 | end
199 |
200 | it "raises error if context has custom type attribute but value is not correct custom type" do
201 | expect { ValidateAfterInvalidCustomTypeTestUseCase.execute() }.to\
202 | raise_error(ActionLogic::AttributeTypeError)
203 | end
204 | end
205 |
206 | describe "presence" do
207 | it "validates presence if presence is specified" do
208 | expect { ValidateAfterPresenceTestUseCase.execute() }.to_not raise_error
209 | end
210 |
211 | it "raises error if context has required key but value is not defined when validation requires presence" do
212 | expect { ValidateAfterInvalidPresenceTestUseCase.execute() }.to\
213 | raise_error(ActionLogic::PresenceError)
214 | end
215 | end
216 |
217 | describe "custom presence" do
218 | it "allows custom presence validation if custom presence is defined" do
219 | expect { ValidateAfterCustomPresenceTestUseCase.execute() }.to_not raise_error
220 | end
221 |
222 | it "raises error if custom presence validation is not satisfied" do
223 | expect { ValidateAfterInvalidCustomPresenceTestUseCase.execute() }.to\
224 | raise_error(ActionLogic::PresenceError)
225 | end
226 |
227 | it "raises error if custom presence validation is not supported" do
228 | expect { ValidateAfterUnrecognizablePresenceTestUseCase.execute() }.to\
229 | raise_error(ActionLogic::UnrecognizablePresenceValidatorError)
230 | end
231 | end
232 | end
233 |
234 | describe "fail!" do
235 | it "returns the context with the correct status and failure message" do
236 | result = FailureTestUseCase.execute()
237 |
238 | expect(result.status).to eq(:failure)
239 | expect(result.message).to eq(Constants::FAILURE_MESSAGE)
240 | end
241 |
242 | it "stops execution of tasks after a task fails the context" do
243 | result = FailureTestUseCase.execute()
244 |
245 | expect(result.first).to eq("first")
246 | expect(result.second).to eq("second")
247 | expect(result.third).to be_nil
248 | end
249 | end
250 |
251 | describe "halt!" do
252 | it "returns the context with the correct status and halt message" do
253 | result = HaltTestUseCase.execute()
254 |
255 | expect(result.status).to eq(:halted)
256 | expect(result.message).to eq(Constants::HALT_MESSAGE)
257 | end
258 |
259 | it "stops execution of tasks after a task halts the context" do
260 | result = HaltTestUseCase.execute()
261 |
262 | expect(result.first).to eq("first")
263 | expect(result.second).to eq("second")
264 | expect(result.third).to be_nil
265 | end
266 | end
267 |
268 | describe "multiple use cases" do
269 | it "does not persist attributes set on contexts from different use cases" do
270 | result = HaltTestUseCase.execute()
271 |
272 | expect(result.first).to eq("first")
273 | expect(result.second).to eq("second")
274 | expect(result.third).to be_nil
275 | expect(result.status).to eq(:halted)
276 |
277 | result2 = ValidateAfterPresenceTestUseCase.execute()
278 |
279 | expect(result2.first).to be_nil
280 | expect(result2.second).to be_nil
281 | expect(result2.integer_test).to eq(1)
282 | expect(result2.success?).to be_truthy
283 | end
284 | end
285 | end
286 | end
287 |
--------------------------------------------------------------------------------
/spec/action_logic/configuration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'action_logic'
3 |
4 | describe ActionLogic do
5 |
6 | subject { described_class }
7 |
8 | around do |example|
9 | described_class.reset!
10 | example.run
11 | described_class.reset!
12 | end
13 |
14 | context "benchmark" do
15 | it "defaults the benchmark configuration option to false" do
16 | expect(described_class.benchmark?).to be_falsey
17 | end
18 |
19 | it "returns true when the benchmark configuration option is set to true" do
20 | described_class.configure do |config|
21 | config.benchmark = true
22 | end
23 |
24 | expect(described_class.benchmark?).to be_truthy
25 | end
26 | end
27 |
28 | context "benchmark_log" do
29 | it "defaults benchmark log file to stdout" do
30 | expect(described_class.benchmark_log).to eq($stdout)
31 | end
32 |
33 | it "returns the log file when the benchmark log configuration option is set" do
34 | temp_file = Object.new
35 |
36 | described_class.configure do |config|
37 | config.benchmark_log = temp_file
38 | end
39 |
40 | expect(described_class.benchmark_log).to eq(temp_file)
41 | end
42 | end
43 |
44 | context "benchmark_formatter" do
45 | it "uses default formatter if a custom formatter is not provided" do
46 | expect(described_class.benchmark_formatter).to be_a(ActionLogic::ActionBenchmark::DefaultFormatter)
47 | end
48 |
49 | it "uses a custom formatter if one is provided" do
50 | class CustomFormatter; end
51 |
52 | described_class.configure do |config|
53 | config.benchmark_formatter = CustomFormatter
54 | end
55 |
56 | expect(described_class.benchmark_formatter).to be_a(CustomFormatter)
57 | end
58 | end
59 |
60 | context "benchmark_handler" do
61 | it "uses a default benchmark handler if a custom benchmark handler is not provided" do
62 | expect(described_class.benchmark_handler).to be_a(ActionLogic::ActionBenchmark::DefaultBenchmarkHandler)
63 | end
64 |
65 | it "uses a custom benchmark handler if one is provided" do
66 | custom_benchmark_handler = -> {}
67 |
68 | described_class.configure do |config|
69 | config.benchmark_handler = custom_benchmark_handler
70 | end
71 |
72 | expect(described_class.benchmark_handler).to eq(custom_benchmark_handler)
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/spec/fixtures/constants.rb:
--------------------------------------------------------------------------------
1 | require 'fixtures/custom_types'
2 |
3 | class Constants
4 | ALL_VALIDATIONS = { :integer_test => { :type => Fixnum, :presence => true },
5 | :float_test => { :type => Float, :presence => true },
6 | :string_test => { :type => String, :presence => true },
7 | :bool_test => { :type => TrueClass, :presence => true },
8 | :hash_test => { :type => Hash, :presence => true },
9 | :array_test => { :type => Array, :presence => true },
10 | :symbol_test => { :type => Symbol, :presence => true },
11 | :nil_test => { :type => NilClass } }
12 |
13 | INVALID_ATTRIBUTES = { :integer_test => nil,
14 | :float_test => nil,
15 | :string_test => nil,
16 | :bool_test => nil,
17 | :hash_test => nil,
18 | :array_test => nil,
19 | :symbol_test => nil,
20 | :nil_test => 1 }
21 |
22 | VALID_ATTRIBUTES = { :integer_test => 1,
23 | :float_test => 1.0,
24 | :string_test => "string",
25 | :bool_test => true,
26 | :hash_test => {},
27 | :array_test => [],
28 | :symbol_test => :symbol,
29 | :nil_test => nil }
30 |
31 | CUSTOM_TYPE_VALIDATION1 = { :custom_type => { :type => CustomType1, :presence => true } }
32 |
33 | CUSTOM_TYPE_ATTRIBUTES1 = { :custom_type => CustomType1.new }
34 |
35 | CUSTOM_TYPE_VALIDATION2 = { :custom_type => { :type => CustomType2, :presence => true } }
36 |
37 | CUSTOM_TYPE_ATTRIBUTES2 = { :custom_type => CustomType2.new }
38 |
39 | PRESENCE_VALIDATION = { :integer_test => { :presence => true } }
40 |
41 | CUSTOM_PRESENCE_VALIDATION = { :array_test => { :presence => ->(array_test) { array_test.any? } } }
42 |
43 | FAILURE_MESSAGE = "error"
44 |
45 | HALT_MESSAGE = "halt"
46 | end
47 |
--------------------------------------------------------------------------------
/spec/fixtures/coordinators.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic'
2 | require 'fixtures/constants'
3 |
4 | # :nocov:
5 | class TestCoordinator1
6 | include ActionLogic::ActionCoordinator
7 |
8 | def call
9 | context.test_coordinator1 = true
10 | end
11 |
12 | def plan
13 | {
14 | TestUseCase1 => { :success => TestUseCase2 },
15 |
16 | TestUseCase2 => { :success => TestUseCase3 },
17 |
18 | TestUseCase3 => { :success => nil }
19 | }
20 | end
21 | end
22 |
23 | class HaltedTestCoordinator1
24 | include ActionLogic::ActionCoordinator
25 |
26 | def call
27 | context.halted_test_coordinator1 = true
28 | end
29 |
30 | def plan
31 | {
32 | HaltedTestUseCase1 => { :success => nil,
33 | :halted => TestUseCase2 },
34 |
35 | TestUseCase2 => { :success => TestUseCase3 },
36 |
37 | TestUseCase3 => { :success => nil }
38 | }
39 | end
40 | end
41 |
42 | class FailureTestCoordinator1
43 | include ActionLogic::ActionCoordinator
44 |
45 | def call
46 | context.failure_test_coordinator1 = true
47 | end
48 |
49 | def plan
50 | {
51 | FailureTestUseCase1 => { :success => nil,
52 | :failure => TestUseCase2 },
53 |
54 | TestUseCase2 => { :success => TestUseCase3 },
55 |
56 | TestUseCase3 => { :success => nil }
57 | }
58 | end
59 | end
60 |
61 | class ValidateBeforeTestCoordinator
62 | include ActionLogic::ActionCoordinator
63 |
64 | validates_before Constants::ALL_VALIDATIONS
65 |
66 | def call
67 | end
68 |
69 | def plan
70 | {
71 | TestUseCase1 => { :success => TestUseCase2 },
72 |
73 | TestUseCase2 => { :success => TestUseCase3 },
74 |
75 | TestUseCase3 => { :success => nil }
76 | }
77 | end
78 | end
79 |
80 | class ValidateBeforeCustomTypeTestCoordinator
81 | include ActionLogic::ActionCoordinator
82 |
83 | validates_before Constants::CUSTOM_TYPE_VALIDATION1
84 |
85 | def call
86 | end
87 |
88 | def plan
89 | {
90 | TestUseCase1 => { :success => TestUseCase2 },
91 |
92 | TestUseCase2 => { :success => TestUseCase3 },
93 |
94 | TestUseCase3 => { :success => nil }
95 | }
96 | end
97 | end
98 |
99 | class ValidateBeforePresenceTestCoordinator
100 | include ActionLogic::ActionCoordinator
101 |
102 | validates_before Constants::PRESENCE_VALIDATION
103 |
104 | def call
105 | end
106 |
107 | def plan
108 | {
109 | TestUseCase1 => { :success => TestUseCase2 },
110 |
111 | TestUseCase2 => { :success => TestUseCase3 },
112 |
113 | TestUseCase3 => { :success => nil }
114 | }
115 | end
116 | end
117 |
118 | class ValidateBeforeCustomPresenceTestCoordinator
119 | include ActionLogic::ActionCoordinator
120 |
121 | validates_before Constants::CUSTOM_PRESENCE_VALIDATION
122 |
123 | def call
124 | end
125 |
126 | def plan
127 | {
128 | TestUseCase1 => { :success => TestUseCase2 },
129 |
130 | TestUseCase2 => { :success => TestUseCase3 },
131 |
132 | TestUseCase3 => { :success => nil }
133 | }
134 | end
135 | end
136 |
137 | class ValidateBeforeUnrecognizablePresenceTestCoordinator
138 | include ActionLogic::ActionCoordinator
139 |
140 | validates_before :integer_test => { :presence => :true }
141 |
142 | def call
143 | end
144 |
145 | def plan
146 | {
147 | TestUseCase1 => { :success => TestUseCase2 },
148 |
149 | TestUseCase2 => { :success => TestUseCase3 },
150 |
151 | TestUseCase3 => { :success => nil }
152 | }
153 | end
154 | end
155 |
156 | class ValidateAfterTestCoordinator
157 | include ActionLogic::ActionCoordinator
158 |
159 | validates_after Constants::ALL_VALIDATIONS
160 |
161 | def call
162 | context.integer_test = 1
163 | context.float_test = 1.0
164 | context.string_test = "string"
165 | context.bool_test = true
166 | context.hash_test = {}
167 | context.array_test = []
168 | context.symbol_test = :symbol
169 | context.nil_test = nil
170 | end
171 |
172 | def plan
173 | {
174 | TestUseCase1 => { :success => TestUseCase2 },
175 |
176 | TestUseCase2 => { :success => TestUseCase3 },
177 |
178 | TestUseCase3 => { :success => nil }
179 | }
180 | end
181 | end
182 |
183 | class ValidateAfterMissingAttributesTestCoordinator
184 | include ActionLogic::ActionCoordinator
185 |
186 | validates_after Constants::ALL_VALIDATIONS
187 |
188 | def call
189 | end
190 |
191 | def plan
192 | {
193 | TestUseCase1 => { :success => TestUseCase2 },
194 |
195 | TestUseCase2 => { :success => TestUseCase3 },
196 |
197 | TestUseCase3 => { :success => nil }
198 | }
199 | end
200 | end
201 |
202 | class ValidateAfterInvalidTypeTestCoordinator
203 | include ActionLogic::ActionCoordinator
204 |
205 | validates_after Constants::ALL_VALIDATIONS
206 |
207 | def call
208 | context.integer_test = nil
209 | context.float_test = nil
210 | context.string_test = nil
211 | context.bool_test = nil
212 | context.hash_test = nil
213 | context.array_test = nil
214 | context.symbol_test = nil
215 | context.nil_test = 1
216 | end
217 |
218 | def plan
219 | {
220 | TestUseCase1 => { :success => TestUseCase2 },
221 |
222 | TestUseCase2 => { :success => TestUseCase3 },
223 |
224 | TestUseCase3 => { :success => nil }
225 | }
226 | end
227 | end
228 |
229 | class ValidateAfterCustomTypeTestCoordinator
230 | include ActionLogic::ActionCoordinator
231 |
232 | validates_after Constants::CUSTOM_TYPE_VALIDATION1
233 |
234 | def call
235 | context.custom_type = CustomType1.new
236 | end
237 |
238 | def plan
239 | {
240 | TestUseCase1 => { :success => TestUseCase2 },
241 |
242 | TestUseCase2 => { :success => TestUseCase3 },
243 |
244 | TestUseCase3 => { :success => nil }
245 | }
246 | end
247 | end
248 |
249 | class ValidateAfterInvalidCustomTypeTestCoordinator
250 | include ActionLogic::ActionCoordinator
251 |
252 | validates_after Constants::CUSTOM_TYPE_VALIDATION2
253 |
254 | def call
255 | context.custom_type = CustomType1.new
256 | end
257 |
258 | def plan
259 | {
260 | TestUseCase1 => { :success => TestUseCase2 },
261 |
262 | TestUseCase2 => { :success => TestUseCase3 },
263 |
264 | TestUseCase3 => { :success => nil }
265 | }
266 | end
267 | end
268 |
269 | class ValidateAfterPresenceTestCoordinator
270 | include ActionLogic::ActionCoordinator
271 |
272 | validates_after Constants::PRESENCE_VALIDATION
273 |
274 | def call
275 | context.integer_test = 1
276 | end
277 |
278 | def plan
279 | {
280 | TestUseCase1 => { :success => TestUseCase2 },
281 |
282 | TestUseCase2 => { :success => TestUseCase3 },
283 |
284 | TestUseCase3 => { :success => nil }
285 | }
286 | end
287 | end
288 |
289 | class ValidateAfterInvalidPresenceTestCoordinator
290 | include ActionLogic::ActionCoordinator
291 |
292 | validates_after Constants::PRESENCE_VALIDATION
293 |
294 | def call
295 | context.integer_test = nil
296 | end
297 |
298 | def plan
299 | {
300 | TestUseCase1 => { :success => TestUseCase2 },
301 |
302 | TestUseCase2 => { :success => TestUseCase3 },
303 |
304 | TestUseCase3 => { :success => nil }
305 | }
306 | end
307 | end
308 |
309 | class ValidateAfterCustomPresenceTestCoordinator
310 | include ActionLogic::ActionCoordinator
311 |
312 | validates_after Constants::CUSTOM_PRESENCE_VALIDATION
313 |
314 | def call
315 | context.array_test = [1]
316 | end
317 |
318 | def plan
319 | {
320 | TestUseCase1 => { :success => TestUseCase2 },
321 |
322 | TestUseCase2 => { :success => TestUseCase3 },
323 |
324 | TestUseCase3 => { :success => nil }
325 | }
326 | end
327 | end
328 |
329 | class ValidateAfterInvalidCustomPresenceTestCoordinator
330 | include ActionLogic::ActionCoordinator
331 |
332 | validates_after Constants::CUSTOM_PRESENCE_VALIDATION
333 |
334 | def call
335 | context.array_test = []
336 | end
337 |
338 | def plan
339 | {
340 | TestUseCase1 => { :success => TestUseCase2 },
341 |
342 | TestUseCase2 => { :success => TestUseCase3 },
343 |
344 | TestUseCase3 => { :success => nil }
345 | }
346 | end
347 | end
348 |
349 | class ValidateAfterUnrecognizablePresenceTestCoordinator
350 | include ActionLogic::ActionCoordinator
351 |
352 | validates_after :integer_test => { :presence => :true }
353 |
354 | def call
355 | context.integer_test = 1
356 | end
357 |
358 | def plan
359 | {
360 | TestUseCase1 => { :success => TestUseCase2 },
361 |
362 | TestUseCase2 => { :success => TestUseCase3 },
363 |
364 | TestUseCase3 => { :success => nil }
365 | }
366 | end
367 | end
368 |
369 | class TestUseCase1
370 | include ActionLogic::ActionUseCase
371 |
372 | def call
373 | context.test_use_case1 = true
374 | end
375 |
376 | def tasks
377 | [TestTask1]
378 | end
379 | end
380 |
381 | class TestHaltUseCase1
382 | include ActionLogic::ActionUseCase
383 |
384 | def call
385 | context.test_use_case1 = true
386 | end
387 |
388 | def tasks
389 | [HaltTestTask1]
390 | end
391 | end
392 |
393 | class TestUseCase2
394 | include ActionLogic::ActionUseCase
395 |
396 | def call
397 | context.test_use_case2 = true
398 | end
399 |
400 | def tasks
401 | [TestTask2]
402 | end
403 | end
404 |
405 | class TestUseCase3
406 | include ActionLogic::ActionUseCase
407 |
408 | def call
409 | context.test_use_case3 = true
410 | end
411 |
412 | def tasks
413 | [TestTask3]
414 | end
415 | end
416 |
417 | class HaltedTestUseCase1
418 | include ActionLogic::ActionUseCase
419 |
420 | def call
421 | context.halted_test_use_case1 = true
422 | end
423 |
424 | def tasks
425 | [HaltedTestTask1]
426 | end
427 | end
428 |
429 | class HaltedTestUseCase2
430 | include ActionLogic::ActionUseCase
431 |
432 | def call
433 | context.halted_test_use_case2 = true
434 | end
435 |
436 | def tasks
437 | [HaltedTestTask2]
438 | end
439 | end
440 |
441 | class HaltedTestUseCase3
442 | include ActionLogic::ActionUseCase
443 |
444 | def call
445 | context.halted_test_use_case3 = true
446 | end
447 |
448 | def tasks
449 | [HaltedTestTask3]
450 | end
451 | end
452 |
453 | class FailureTestUseCase1
454 | include ActionLogic::ActionUseCase
455 |
456 | def call
457 | context.failure_test_use_case1 = true
458 | end
459 |
460 | def tasks
461 | [FailureTestTask1]
462 | end
463 | end
464 |
465 | class FailureTestUseCase2
466 | include ActionLogic::ActionUseCase
467 |
468 | def call
469 | context.failure_test_use_case2 = true
470 | end
471 |
472 | def tasks
473 | [FailureTestTask2]
474 | end
475 | end
476 |
477 | class FailureTestUseCase3
478 | include ActionLogic::ActionUseCase
479 |
480 | def call
481 | context.failure_test_use_case3 = true
482 | end
483 |
484 | def tasks
485 | [FailureTestTask3]
486 | end
487 | end
488 |
489 | class TestTask1
490 | include ActionLogic::ActionTask
491 |
492 | def call
493 | context.test_task1 = true
494 | end
495 | end
496 |
497 | class TestTask2
498 | include ActionLogic::ActionTask
499 |
500 | def call
501 | context.test_task2 = true
502 | end
503 | end
504 |
505 | class TestTask3
506 | include ActionLogic::ActionTask
507 |
508 | def call
509 | context.test_task3 = true
510 | end
511 | end
512 |
513 | class HaltedTestTask1
514 | include ActionLogic::ActionTask
515 |
516 | def call
517 | context.halted_test_task1 = true
518 | context.halt!
519 | end
520 | end
521 |
522 | class HaltedTestTask2
523 | include ActionLogic::ActionTask
524 |
525 | def call
526 | context.halted_test_task2 = true
527 | context.halt!
528 | end
529 | end
530 |
531 | class HaltedTestTask3
532 | include ActionLogic::ActionTask
533 |
534 | def call
535 | context.halted_test_task3 = true
536 | context.halt!
537 | end
538 | end
539 |
540 | class FailureTestTask1
541 | include ActionLogic::ActionTask
542 |
543 | def call
544 | context.failure_test_task1 = true
545 | context.fail!
546 | end
547 | end
548 |
549 | class FailureTestTask2
550 | include ActionLogic::ActionTask
551 |
552 | def call
553 | context.failure_test_task2 = true
554 | context.fail!
555 | end
556 | end
557 |
558 | class FailureTestTask3
559 | include ActionLogic::ActionTask
560 |
561 | def call
562 | context.failure_test_task3 = true
563 | context.fail!
564 | end
565 | end
566 | # :nocov:
567 |
--------------------------------------------------------------------------------
/spec/fixtures/custom_types.rb:
--------------------------------------------------------------------------------
1 | class CustomType1
2 | end
3 |
4 | class CustomType2
5 | end
6 |
--------------------------------------------------------------------------------
/spec/fixtures/tasks.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic'
2 | require 'fixtures/custom_types'
3 | require 'fixtures/constants'
4 | # :nocov:
5 | class SimpleTestTask
6 | include ActionLogic::ActionTask
7 |
8 | def call
9 | context.new_attribute = true
10 | end
11 | end
12 |
13 | class ValidateAroundTestTask
14 | include ActionLogic::ActionTask
15 |
16 | validates_around Constants::ALL_VALIDATIONS
17 |
18 | def call
19 | end
20 | end
21 |
22 | class ValidateAroundCustomTypeTestTask
23 | include ActionLogic::ActionTask
24 |
25 | validates_around :custom_type => { :type => CustomType1, :presence => true }
26 |
27 | def call
28 | end
29 | end
30 |
31 | class ValidateAroundUnrecognizablePresenceTestTask
32 | include ActionLogic::ActionTask
33 |
34 | validates_around :integer_test => { :presence => :true }
35 |
36 | def call
37 | end
38 | end
39 |
40 | class ValidateAroundPresenceTestTask
41 | include ActionLogic::ActionTask
42 |
43 | validates_around :integer_test => { :presence => true }
44 |
45 | def call
46 | end
47 | end
48 |
49 | class ValidateAroundCustomPresenceTestTask
50 | include ActionLogic::ActionTask
51 |
52 | validates_around :array_test => { :presence => ->(array_test) { array_test.any? } }
53 |
54 | def call
55 | end
56 |
57 | def tasks
58 | []
59 | end
60 | end
61 |
62 | class ValidateBeforeTestTask
63 | include ActionLogic::ActionTask
64 |
65 | validates_before Constants::ALL_VALIDATIONS
66 |
67 | def call
68 | end
69 | end
70 |
71 | class ValidateBeforeCustomTypeTestTask
72 | include ActionLogic::ActionTask
73 |
74 | validates_before :custom_type => { :type => CustomType1, :presence => true }
75 |
76 | def call
77 | end
78 | end
79 |
80 | class ValidateBeforeUnrecognizablePresenceTestTask
81 | include ActionLogic::ActionTask
82 |
83 | validates_before :integer_test => { :presence => :true }
84 |
85 | def call
86 | end
87 | end
88 |
89 | class ValidateBeforePresenceTestTask
90 | include ActionLogic::ActionTask
91 |
92 | validates_before :integer_test => { :presence => true }
93 |
94 | def call
95 | end
96 | end
97 |
98 | class ValidateBeforeCustomPresenceTestTask
99 | include ActionLogic::ActionTask
100 |
101 | validates_before :array_test => { :presence => ->(array_test) { array_test.any? } }
102 |
103 | def call
104 | end
105 |
106 | def tasks
107 | []
108 | end
109 | end
110 |
111 | class ValidateAfterTestTask
112 | include ActionLogic::ActionTask
113 |
114 | validates_after Constants::ALL_VALIDATIONS
115 |
116 | def call
117 | context.integer_test = 1
118 | context.float_test = 1.0
119 | context.string_test = "string"
120 | context.bool_test = true
121 | context.hash_test = {}
122 | context.array_test = []
123 | context.symbol_test = :symbol
124 | context.nil_test = nil
125 | end
126 | end
127 |
128 | class ValidateAfterMissingAttributesTestTask
129 | include ActionLogic::ActionTask
130 |
131 | validates_after Constants::ALL_VALIDATIONS
132 |
133 | def call
134 | end
135 | end
136 |
137 | class ValidateAfterInvalidTypeTestTask
138 | include ActionLogic::ActionTask
139 |
140 | validates_after Constants::ALL_VALIDATIONS
141 |
142 | def call
143 | context.integer_test = nil
144 | context.float_test = nil
145 | context.string_test = nil
146 | context.bool_test = nil
147 | context.hash_test = nil
148 | context.array_test = nil
149 | context.symbol_test = nil
150 | context.nil_test = 1
151 | end
152 | end
153 |
154 | class ValidateAfterCustomTypeTestTask
155 | include ActionLogic::ActionTask
156 |
157 | validates_after :custom_type => { :type => CustomType1, :presence => true }
158 |
159 | def call
160 | context.custom_type = CustomType1.new
161 | end
162 | end
163 |
164 | class ValidateAfterInvalidCustomTypeTestTask
165 | include ActionLogic::ActionTask
166 |
167 | validates_after :custom_type => { :type => CustomType2, :presence => true }
168 |
169 | def call
170 | context.custom_type = CustomType1.new
171 | end
172 | end
173 |
174 | class ValidateAfterPresenceTestTask
175 | include ActionLogic::ActionTask
176 |
177 | validates_after :integer_test => { :presence => true }
178 |
179 | def call
180 | context.integer_test = 1
181 | end
182 | end
183 |
184 | class ValidateAfterInvalidPresenceTestTask
185 | include ActionLogic::ActionTask
186 |
187 | validates_after :integer_test => { :presence => true }
188 |
189 | def call
190 | context.integer_test = nil
191 | end
192 | end
193 |
194 | class ValidateAfterCustomPresenceTestTask
195 | include ActionLogic::ActionTask
196 |
197 | validates_after :array_test => { :presence => ->(array_test) { array_test.any? } }
198 |
199 | def call
200 | context.array_test = [1]
201 | end
202 | end
203 |
204 | class ValidateAfterInvalidCustomPresenceTestTask
205 | include ActionLogic::ActionTask
206 |
207 | validates_after :array_test => { :presence => ->(array_test) { array_test.any? } }
208 |
209 | def call
210 | context.array_test = []
211 | end
212 | end
213 |
214 | class ValidateAfterUnrecognizablePresenceTestTask
215 | include ActionLogic::ActionTask
216 |
217 | validates_after :integer_test => { :presence => :true }
218 |
219 | def call
220 | context.integer_test = 1
221 | end
222 | end
223 |
224 | class ErrorHandlerTestTask
225 | include ActionLogic::ActionTask
226 |
227 | def call
228 | raise
229 | end
230 |
231 | def error(e)
232 | context.e = e
233 | end
234 | end
235 |
236 | class ErrorHandlerInvalidAttributesBeforeTestTask
237 | include ActionLogic::ActionTask
238 |
239 | validates_before Constants::ALL_VALIDATIONS
240 |
241 | def call
242 | raise
243 | end
244 |
245 | def error(e)
246 | context.error = "error"
247 | end
248 | end
249 |
250 | class ErrorHandlerInvalidAttributesAfterTestTask
251 | include ActionLogic::ActionTask
252 |
253 | validates_after Constants::ALL_VALIDATIONS
254 |
255 | def call
256 | raise
257 | end
258 |
259 | def error(e)
260 | context.error = "error"
261 | end
262 | end
263 |
264 | class MissingErrorHandlerTestTask
265 | include ActionLogic::ActionTask
266 |
267 | def call
268 | raise
269 | end
270 | end
271 |
272 | class FailureTestTask
273 | include ActionLogic::ActionTask
274 |
275 | def call
276 | context.fail!(Constants::FAILURE_MESSAGE)
277 | end
278 | end
279 |
280 | class HaltTestTask
281 | include ActionLogic::ActionTask
282 |
283 | def call
284 | context.halt!(Constants::HALT_MESSAGE)
285 | end
286 | end
287 |
288 | class UseCaseTestTask1
289 | include ActionLogic::ActionTask
290 |
291 | def call
292 | context.first = "first"
293 | end
294 | end
295 |
296 | class UseCaseTestTask2
297 | include ActionLogic::ActionTask
298 |
299 | def call
300 | context.second = "second"
301 | end
302 | end
303 |
304 | class UseCaseTestTask3
305 | include ActionLogic::ActionTask
306 |
307 | def call
308 | context.third = "third"
309 | end
310 | end
311 |
312 | class UseCaseFailureTestTask
313 | include ActionLogic::ActionTask
314 |
315 | def call
316 | context.fail!(Constants::FAILURE_MESSAGE)
317 | end
318 | end
319 |
320 | class UseCaseHaltTestTask
321 | include ActionLogic::ActionTask
322 |
323 | def call
324 | context.halt!(Constants::HALT_MESSAGE)
325 | end
326 | end
327 | # :nocov:
328 |
--------------------------------------------------------------------------------
/spec/fixtures/use_cases.rb:
--------------------------------------------------------------------------------
1 | require 'action_logic'
2 | require 'fixtures/tasks'
3 | require 'fixtures/constants'
4 |
5 | # :nocov:
6 | class SimpleTestUseCase
7 | include ActionLogic::ActionUseCase
8 |
9 | def call
10 | end
11 |
12 | def tasks
13 | [SimpleTestTask]
14 | end
15 | end
16 |
17 | class SimpleTestUseCase2
18 | include ActionLogic::ActionUseCase
19 |
20 | def call
21 | end
22 |
23 | def tasks
24 | [UseCaseTestTask1,
25 | UseCaseTestTask2]
26 | end
27 | end
28 |
29 | class SimpleTestUseCase3
30 | include ActionLogic::ActionUseCase
31 |
32 | def call
33 | context.second = "defined in use case"
34 | end
35 |
36 | def tasks
37 | [UseCaseTestTask1]
38 | end
39 | end
40 |
41 | class NoTaskTestUseCase
42 | include ActionLogic::ActionUseCase
43 |
44 | def call
45 | end
46 |
47 | def tasks
48 | []
49 | end
50 | end
51 |
52 | class ValidateAroundTestUseCase
53 | include ActionLogic::ActionUseCase
54 |
55 | validates_around Constants::ALL_VALIDATIONS
56 |
57 | def call
58 | end
59 |
60 | def tasks
61 | [UseCaseTestTask1,
62 | UseCaseTestTask2]
63 | end
64 | end
65 |
66 | class ValidateAroundCustomTypeTestUseCase
67 | include ActionLogic::ActionUseCase
68 |
69 | validates_around :custom_type => { :type => CustomType1, :presence => true }
70 |
71 | def call
72 | end
73 |
74 | def tasks
75 | [UseCaseTestTask1,
76 | UseCaseTestTask2]
77 | end
78 | end
79 |
80 | class ValidateAroundUnrecognizablePresenceTestUseCase
81 | include ActionLogic::ActionUseCase
82 |
83 | validates_around :integer_test => { :presence => :true }
84 |
85 | def call
86 | end
87 |
88 | def tasks
89 | [UseCaseTestTask1,
90 | UseCaseTestTask2]
91 | end
92 | end
93 |
94 | class ValidateAroundPresenceTestUseCase
95 | include ActionLogic::ActionUseCase
96 |
97 | validates_around :integer_test => { :presence => true }
98 |
99 | def call
100 | end
101 |
102 | def tasks
103 | [UseCaseTestTask1,
104 | UseCaseTestTask2]
105 | end
106 | end
107 |
108 | class ValidateAroundCustomPresenceTestUseCase
109 | include ActionLogic::ActionUseCase
110 |
111 | validates_around :array_test => { :presence => ->(array_test) { array_test.any? } }
112 |
113 | def call
114 | end
115 |
116 | def tasks
117 | [UseCaseTestTask1,
118 | UseCaseTestTask2]
119 | end
120 | end
121 |
122 | class ValidateBeforeTestUseCase
123 | include ActionLogic::ActionUseCase
124 |
125 | validates_before Constants::ALL_VALIDATIONS
126 |
127 | def call
128 | end
129 |
130 | def tasks
131 | [UseCaseTestTask1,
132 | UseCaseTestTask2]
133 | end
134 | end
135 |
136 | class ValidateBeforePresenceTestUseCase
137 | include ActionLogic::ActionUseCase
138 |
139 | validates_before Constants::PRESENCE_VALIDATION
140 |
141 | def call
142 | end
143 |
144 | def tasks
145 | [UseCaseTestTask1,
146 | UseCaseTestTask2]
147 | end
148 | end
149 |
150 | class ValidateBeforeCustomPresenceTestUseCase
151 | include ActionLogic::ActionUseCase
152 |
153 | validates_before Constants::CUSTOM_PRESENCE_VALIDATION
154 |
155 | def call
156 | end
157 |
158 | def tasks
159 | [UseCaseTestTask1,
160 | UseCaseTestTask2]
161 | end
162 | end
163 |
164 | class ValidateBeforeCustomTypeTestUseCase
165 | include ActionLogic::ActionUseCase
166 |
167 | validates_before Constants::CUSTOM_TYPE_VALIDATION1
168 |
169 | def call
170 | end
171 |
172 | def tasks
173 | [UseCaseTestTask1,
174 | UseCaseTestTask2]
175 | end
176 | end
177 |
178 | class ValidateBeforeUnrecognizablePresenceTestUseCase
179 | include ActionLogic::ActionUseCase
180 |
181 | validates_before :integer_test => { :presence => :true }
182 |
183 | def call
184 | end
185 |
186 | def tasks
187 | [UseCaseTestTask1,
188 | UseCaseTestTask2]
189 | end
190 | end
191 |
192 | class ValidateBeforeMixedTypeAndPresenceUseCase
193 | include ActionLogic::ActionUseCase
194 |
195 | validates_before odd_integer_test: { type: Integer, presence: ->(i) { i % 2 != 0 }, type: Integer },
196 | string_test: { presence: false, type: String }
197 |
198 | def call
199 | end
200 |
201 | def tasks
202 | [UseCaseTestTask1,
203 | UseCaseTestTask2]
204 | end
205 | end
206 |
207 | class ValidateAfterTestUseCase
208 | include ActionLogic::ActionUseCase
209 |
210 | validates_after Constants::ALL_VALIDATIONS
211 |
212 | def call
213 | context.integer_test = 1
214 | context.float_test = 1.0
215 | context.string_test = "string"
216 | context.bool_test = true
217 | context.hash_test = {}
218 | context.array_test = []
219 | context.symbol_test = :symbol
220 | context.nil_test = nil
221 | end
222 |
223 | def tasks
224 | [UseCaseTestTask3]
225 | end
226 | end
227 |
228 | class ValidateAfterMissingAttributesTestUseCase
229 | include ActionLogic::ActionUseCase
230 |
231 | validates_after Constants::ALL_VALIDATIONS
232 |
233 | def call
234 | end
235 |
236 | def tasks
237 | [UseCaseTestTask1,
238 | UseCaseTestTask2]
239 | end
240 | end
241 |
242 | class ValidateAfterInvalidTypeTestUseCase
243 | include ActionLogic::ActionUseCase
244 |
245 | validates_after Constants::ALL_VALIDATIONS
246 |
247 | def call
248 | context.integer_test = nil
249 | context.float_test = nil
250 | context.string_test = nil
251 | context.bool_test = nil
252 | context.hash_test = nil
253 | context.array_test = nil
254 | context.symbol_test = nil
255 | context.nil_test = 1
256 | end
257 |
258 | def tasks
259 | [UseCaseTestTask1,
260 | UseCaseTestTask2]
261 | end
262 | end
263 |
264 | class ValidateAfterCustomTypeTestUseCase
265 | include ActionLogic::ActionUseCase
266 |
267 | validates_after Constants::CUSTOM_TYPE_VALIDATION1
268 |
269 | def call
270 | context.custom_type = CustomType1.new
271 | end
272 |
273 | def tasks
274 | [UseCaseTestTask1,
275 | UseCaseTestTask2]
276 | end
277 | end
278 |
279 | class ValidateAfterInvalidCustomTypeTestUseCase
280 | include ActionLogic::ActionUseCase
281 |
282 | validates_after Constants::CUSTOM_TYPE_VALIDATION2
283 |
284 | def call
285 | context.custom_type = CustomType1.new
286 | end
287 |
288 | def tasks
289 | [UseCaseTestTask1,
290 | UseCaseTestTask2]
291 | end
292 | end
293 |
294 | class ValidateAfterPresenceTestUseCase
295 | include ActionLogic::ActionUseCase
296 |
297 | validates_after Constants::PRESENCE_VALIDATION
298 |
299 | def call
300 | context.integer_test = 1
301 | end
302 |
303 | def tasks
304 | [UseCaseTestTask3]
305 | end
306 | end
307 |
308 | class ValidateAfterInvalidPresenceTestUseCase
309 | include ActionLogic::ActionUseCase
310 |
311 | validates_after Constants::PRESENCE_VALIDATION
312 |
313 | def call
314 | context.integer_test = nil
315 | end
316 |
317 | def tasks
318 | [UseCaseTestTask1,
319 | UseCaseTestTask2]
320 | end
321 | end
322 |
323 | class ValidateAfterCustomPresenceTestUseCase
324 | include ActionLogic::ActionUseCase
325 |
326 | validates_after Constants::CUSTOM_PRESENCE_VALIDATION
327 |
328 | def call
329 | context.array_test = [1]
330 | end
331 |
332 | def tasks
333 | [UseCaseTestTask1,
334 | UseCaseTestTask2]
335 | end
336 | end
337 |
338 | class ValidateAfterInvalidCustomPresenceTestUseCase
339 | include ActionLogic::ActionUseCase
340 |
341 | validates_after Constants::CUSTOM_PRESENCE_VALIDATION
342 |
343 | def call
344 | context.array_test = []
345 | end
346 |
347 | def tasks
348 | [UseCaseTestTask1,
349 | UseCaseTestTask2]
350 | end
351 | end
352 |
353 | class ValidateAfterUnrecognizablePresenceTestUseCase
354 | include ActionLogic::ActionUseCase
355 |
356 | validates_after :integer_test => { :presence => :true }
357 |
358 | def call
359 | context.integer_test = 1
360 | end
361 |
362 | def tasks
363 | [UseCaseTestTask1,
364 | UseCaseTestTask2]
365 | end
366 | end
367 |
368 | class FailureTestUseCase
369 | include ActionLogic::ActionUseCase
370 |
371 | def call
372 | end
373 |
374 | def tasks
375 | [UseCaseTestTask1,
376 | UseCaseTestTask2,
377 | UseCaseFailureTestTask,
378 | UseCaseTestTask3]
379 | end
380 | end
381 |
382 | class HaltTestUseCase
383 | include ActionLogic::ActionUseCase
384 |
385 | def call
386 | end
387 |
388 | def tasks
389 | [UseCaseTestTask1,
390 | UseCaseTestTask2,
391 | UseCaseHaltTestTask,
392 | UseCaseTestTask3]
393 | end
394 | end
395 | # :nocov:
396 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 |
3 | $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4 | $LOAD_PATH << File.join(File.dirname(__FILE__))
5 |
6 | SimpleCov.start do
7 | add_filter 'spec/fixtures'
8 | end
9 |
10 | if ENV['CI']
11 | require 'codecov'
12 | SimpleCov.formatter = SimpleCov::Formatter::Codecov
13 | end
14 |
15 | require 'action_logic'
16 |
17 | class CustomFormatter < ActionLogic::ActionBenchmark::DefaultFormatter
18 | def log_coordinator(benchmark_result, execution_context_name)
19 | benchmark_log.puts("The ActionCoordinator #{execution_context_name} took #{benchmark_result} to complete.")
20 | end
21 |
22 | def log_use_case(benchmark_result, execution_context_name)
23 | benchmark_log.puts("The ActionUseCase #{execution_context_name} took #{benchmark_result} to complete.")
24 | end
25 |
26 | def log_task(benchmark_result, execution_context_name)
27 | benchmark_log.puts("The ActionTask #{execution_context_name} took #{benchmark_result} to complete.")
28 | end
29 | end
30 |
31 | class CustomHandler
32 | def call
33 | yield
34 | "this is the custom handler"
35 | end
36 | end
37 |
38 | if ENV['BENCHMARK']
39 | ActionLogic.configure do |config|
40 | config.benchmark = true
41 | config.benchmark_log = File.open("benchmark.log", "w")
42 | config.benchmark_formatter = CustomFormatter
43 | config.benchmark_handler = CustomHandler.new
44 | end
45 | end
46 |
47 | RSpec.configure do |c|
48 | c.fail_fast = true
49 | c.color = true
50 | c.formatter = 'documentation'
51 | c.order = 'rand'
52 | end
53 |
--------------------------------------------------------------------------------