Freshly/flow

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# Flow

[![Gem Version](https://badge.fury.io/rb/flow.svg)](https://badge.fury.io/rb/flow)
[![Build Status](https://semaphoreci.com/api/v1/freshly/flow/branches/main/badge.svg)](https://semaphoreci.com/freshly/flow)
[![Maintainability](https://api.codeclimate.com/v1/badges/02131658005b10c289e0/maintainability)](https://codeclimate.com/github/Freshly/flow/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/02131658005b10c289e0/test_coverage)](https://codeclimate.com/github/Freshly/flow/test_coverage)

## Installation

Add this line to your application's Gemfile:

```ruby
gem "flow"
```

Then, in your project directory:

```bash
$ bundle install
$ rails generate flow:install
```

## What is Flow?

Flow is a [SOLID](https://en.wikipedia.org/wiki/SOLID) implementation of the [Command Pattern](https://en.wikipedia.org/wiki/Command_pattern) for Ruby on Rails.

Flows allow you to encapsulate your application's [business logic](http://en.wikipedia.org/wiki/Business_logic) into a set of extensible and reusable objects.

## Quickstart Example

Install Flow to your Rails project:
```bash
$ rails generate flow:install
```

Then define `State`, `Operation`(s), and `Flow` objects.

### State

A `State` object defines data that is to be read or written in `Operation` objects throughout the `Flow`. There are several types of data that can be defined, such as `argument`, `option`, and `output`.

```bash
$ rails generate flow:state Charge
```

```ruby
# app/states/charge_state.rb

class ChargeState < ApplicationState
  # @!attribute [r]
  # Order hash, readonly, required
  argument :order
  # @!attribute [r]
  # User model instance readonly, required
  argument :user

  # @!attribute [r]
  # PaymentMethod model instance readonly, optional
  option :payment_method

  # @!attribute [rw]
  # Charge model instance readwrite, required
  output :charge
end
```

### Operations

`Operation` objects execute some procedure defined in a `#behavior` method and can read and write to `State` data via defined accessor methods.

```bash
$ rails generate flow:operation CreateCharge
```

```ruby
# app/operations/create_charge.rb

class CreateCharge < ApplicationOperation
  # @!attribute [r]
  # Order hash, readonly
  state_reader :order
  # @!attribute [r]
  # User model instance readonly
  state_reader :user
  # @!attribute [r]
  # PaymentMethod model instance readonly
  state_reader :payment_method

  # @!attribute [rw]
  # Charge model instance readwrite
  state_writer :charge

  def behavior
    state.charge = Charge.create(payment_method: payment_method, order: order, user: user)
  end

  private

  def payment_method
    payment_method.present? ? payment_method : user.default_payment_method
  end
end
```

Use failure methods when an `Operation` and `Flow` should fail and no longer run:

```bash
$ rails generate flow:operation SubmitCharge
```

```ruby
# app/operations/submit_charge.rb

class SubmitCharge < ApplicationOperation
  # @!method charge_unsuccessful_failure!(data = {})
  # Raises, stops the Operation and Flow, takes unstructured data hash
  failure :charge_unsuccessful

  # @!attribute [rw]
  # Charge model instance read only
  state_reader :charge

  def behavior
    charge_unsuccessful_failure!(response_body: response.body) unless success?

    charge.update(success: true)
  end

  private

  def success?
    response.body.success == "true"
  end

  def response
    PaymentProcessorClient.submit_charge(charge)
  end
  memoize :response
end
```

### Flow

A `Flow` object is composed of one or more ordered `Operation`s. Changes to the state will persist from one `Operation` to the next:

```bash
$ rails generate flow Charge
```

```ruby
# app/flow/charge_flow.rb

class ChargeFlow < ApplicationFlow
  operations CreateCharge,
             SubmitCharge
end
```

### Usage

Trigger the `Flow` in your code with `State` inputs:

```ruby
flow_input = {
  order: order,
  user: current_user,
  payment_method: visa_credit_card,
}

flow = ChargeFlow.trigger(flow_input)
```

Arguments defined on `State` are required when triggering a `Flow`, options are optional:

```
> ChargeFlow.trigger({})
ArgumentError: Missing arguments: order, user
```

State output can be accessed from the flow instance:

```
> flow = ChargeFlow.trigger(flow_input)

> flow.state.charge
=> #<Charge:0x00007fd5c5cda080 ... >
```

Success of the triggered `Flow` can be determined with these methods:

```
> flow.success?
=> true

> flow.failed?
=> false
```

If the `Flow` fails you can see the failures on the instance:

```
# some flow that results in a failure...
> flow = ChargeFlow.trigger(flow_input)

> flow.success?
=> false

# get the failure
> flow.operation_failure.problem
=> :charge_unsuccessful

# access unstructured hash passed into failure method
> flow.operation_failure.details.response_body
=> { some_response_body_here: ... }
```

## Wiki

Learn more with our wiki [Getting Started](https://github.com/Freshly/flow/wiki/Getting-Started#installation) page.

You also can download wiki to have offline access.
Just simply do:

`git clone git@github.com:Freshly/flow.wiki.git`


## License

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