listminut/use_cases

View on GitHub
README.md

Summary

Maintainability
Test Coverage
[![Maintainability](https://api.codeclimate.com/v1/badges/b47701c9616987832bba/maintainability)](https://codeclimate.com/github/listminut/use_cases/maintainability)

[![Test Coverage](https://api.codeclimate.com/v1/badges/b47701c9616987832bba/test_coverage)](https://codeclimate.com/github/listminut/use_cases/test_coverage)

# UseCases

`UseCases` is a dry-ecosystem-based gem that implements a DSL for the use case pattern using the railway programming paradigm.

It's concept is largely based on `dry-transaction` but does not use it behind the scenes. Instead it relies on other `dry` libraries like [dry-validation](https://dry-rb.org/gems/dry-validation/), [dry-events](https://dry-rb.org/gems/dry-validation/) and [dry-monads](https://dry-rb.org/gems/dry-validation/) to implement a DSL that can be flexible enough for your needs.

### Including UseCase

Including the `UseCase` module ensures that your class implements the base use case [Base DSL](#the-base-dsl).

```ruby
class Users::Create
  include UseCase
end
````

In order to add optional modules (optins), use the following notation:

```ruby
class Users::Create
  include UseCase[:validated, :transactional]
end
```

### Using a UseCase

```ruby
create_user = Users::Create.new
params = { first_name: 'Don', last_name: 'Quixote' }

result = create_user.call(params, current_user)

# Checking if succeeded
result.success?

# Checking if failed
result.failure?

# Getting return value
result.value!
```

Or with using dry-matcher by passing a block:

```ruby
create_user = Users::Create.new
params = { first_name: 'Don', last_name: 'Quixote' }

create_user.call(params, current_user) do |on|
  on.success do |user|
    puts "#{user.first_name} created!"
  end

  on.failure do |(code, message)|
    puts "Failure (#{code}): #{message}"
  end
end
```

### Available Optins


| Optin | Description |
|---|---|
| `:authorized` | Adds an extra `authorize` step macro, used to check user permissions. |
| `:prepared` | Adds an extra `prepare` step macro, used to run some code before the use case runs. |
| `:transactional` | Calls `#transaction` on a given `transaction_handler` object around the use case execution |
| `:validated` | Adds all methods of `dry-transaction` to the use case DSL, which run validations on the received `params` object. | 

### The base DSL

Use cases implements a DSL similar to dry-transaction, using the [Railway programming paradigm](https://fsharpforfunandprofit.com/rop/).


Each step macro has a different use case, and so a different subset of available options, different expectations in return values, and interaction with the following step.

By taking a simple look at the definition of a use case, anyone should be able to understand the business rules it emcompasses. For that it is necessary to understand the following matrix.


|  | Rationale for use | Accepted Options | Expected return | Passes return value |
|---|---|---|---|---|
| **step** | This step has some complexity, and it can fail or succeed. | `with`, `pass` | `Success`/ `Failure` | ✅ |
| **check** | This step checks sets some rules for the operation, usually verifying that domain models fulfil some conditions.  | `with`, `pass`, `failure`, `failure_message` | `boolean` | ❌ |
| **map** | Nothing should go wrong within this step. If it does, it's an unexpected application error. | `with`, `pass` | `any` | ✅ |
| **try** | We expect that, in some cases, errors will occur, and the operation fails in that case. | `catch`, `with`, `pass`, `failure`, `failure_message` | `any` | ✅ |
| **tee** | We don't care if this step succeeds or fails, it's used for non essential side effects. | `with`, `pass` | `any` | ❌ |

#### Optional steps

|  | Rationale for use | Accepted Options | Expected return | Passes return value |
|---|---|---|---|---|
| **enqueue** *(requires ActiveJob defined) | The same as a `tee`, but executed later to perform non-essential expensive operations. | `with`, `pass`, and sidekiq options | `any` | ❌ |
| **authorize**<br> *(requires authorized) | Performs authorization on the current user, by running a  `check` which, in case of failure, always returns an `unauthorized` failure. | `with`, `pass`, `failure_message` | `boolean` | ❌ |
| **prepare**<br> *(requires prepared) | Adds a `tee` step that always runs first. Used to mutate params if necessary. | `with`, `pass` | `any` | ❌ |


### Defining Steps

Defining a step can be done in the body of the use case.

```ruby
class Users::DeleteAccount
  include UseCases[:validated, :transactional, :validated]

  step :do_something, {}
```

In real life, a simple use case would look something like:

```ruby
class Users::DeleteAccount
  include UseCases[:validated, :transactional, :validated]

  params do
    required(:id).filled(:str?)
  end

  authorize :user_owns_account?, failure_message: 'Cannot delete account'
  try :load_account, catch: ActiveRecord::RecordNotFound, failure: :account_not_found, failure_message: 'Account not found'
  map :delete_account
  enqueue :send_farewell_email

  private 

  def user_owns_account?(_previous_step_input, params, current_user)
    current_user.account_id == params[:id]
  end

  def load_account(_previous_step_input, params, _current_user)
    Account.find_by!(user_id: params[:id])
  end

  def delete_account(account, _params, _current_user)
    account.destroy!
  end

  # since this executed async, all args are serialized
  def send_farewell_email(account_attrs, params, current_user_attrs)
    user = User.find(params[:id])
    UserMailer.farewell(user).deliver_now!
  end
end
```

#### Available Options

| Name | Description | Expected Usage |
|---|---|---|
| `with` | Retrieves the callable object used to perform the step. |<em><br> Symbol: `send(options[:with])` <br> String: `UseCases.config.container[options[:with]]` <br> Class: `options[:with]`</em>  |
| `pass` | An array of the arguments to pass to the object set by `with`. <br> _options: params, current_user & previous_step_result_ | Array\<Symbol> |
| `failure` | The code passed to the Failure object. | Symbol / String |
| `failure_message` | The string message passed to the Failure object. | Symbol / String | 
| `catch` | Array of error classes to rescue from. | Array\<Exception>

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'use_cases'
```

And then execute:

    $ bundle install

Or install it yourself as:

    $ gem install use_cases

## Usage

To get a good basis to get started on `UseCases`, make sure to read [dry-transaction](https://dry-rb.org/gems/dry-transaction/0.13/)'s documentation first.

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/use_cases. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/use_cases/blob/master/CODE_OF_CONDUCT.md).

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## Code of Conduct

Everyone interacting in the UseCases project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/use_cases/blob/master/CODE_OF_CONDUCT.md).