├── .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 | [![Build Status](https://travis-ci.org/rewinfrey/ActionLogic.svg?branch=master)](https://travis-ci.org/rewinfrey/ActionLogic) 4 | [![Gem Version](https://badge.fury.io/rb/action_logic.svg)](https://badge.fury.io/rb/action_logic) 5 | [![Code Climate](https://codeclimate.com/github/rewinfrey/action_logic/badges/gpa.svg)](https://codeclimate.com/github/rewinfrey/action_logic) 6 | [![coverage](https://codecov.io/github/rewinfrey/ActionLogic/coverage.svg?branch=master)](https://codecov.io/github/rewinfrey/ActionLogic?branch=master) 7 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](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 | --------------------------------------------------------------------------------