Fluxo

Provides a simple and powerful way to create operations service objects for complex workflows.

Installation

Add this line to your application's Gemfile:

gem 'fluxo'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install fluxo

Usage

Minimal operation definition:

class MyOperation < Fluxo::Operation
  def call!(**)
    Success(:ok)
  end
end

And then just use the opperation by calling:

result = MyOperation.call
result.success? # => true
result.value # => :ok

In order to execute an operation with parameters, you just need to pass them to the call method:

class MyOperation < Fluxo::Operation
  def call!(param1:, param2:)
    Success(:ok)
  end
end

Operation Result

The execution result of an operation is a Fluxo::Result object. There are three types of results:

  • :ok: the operation was successful
  • :failure: the operation failed
  • :exception: the operation raised an error

Use the Success and Failure methods to create results accordingly.

class AgeCheckOperation < Fluxo::Operation
  self.strict = false # By default, operations are strict. You must it to catch errors and use on_error hook.

  def call!(age:)
    age >= 18 ? Success('ok') : Failure('too young')
  end
end

result = AgeCheckOperation.call(age: 16) # #<Fluxo::Result @value="too young", @type=:failure>
result.success?                          # false
result.error?                            # false
result.failure?                          # true
result.value                             # "too young"

result = AgeCheckOperation.call(age: 18) # #<Fluxo::Result @value="ok", @type=:ok>
result.success?                          # true
result.error?                            # false
result.failure?                          # false
result.value                             # "ok"

The result also provides on_success, on_failure and on_error methods to define callbacks for the :ok and :failure results.

AgeCheckOperation.call(age: 18)
  .on_success { |result| puts result.value }
  .on_failure { |_result| puts "Sorry, you are too young" }

You can also define multiple callbacks for the opportunity result. The callbacks are executed in the order they were defined. You can filter which callbacks are executed by specifying an identifier to the Success(id) { } or Failure(id) { } methods along with its value as a block.

class AgeCategoriesOperation < Fluxo::Operation
  def call!(age:)
    case age
    when 0..14
      Failure(:child) { "Sorry, you are too young" }
    when 15..17
      Failure(:teenager) { "You are a teenager" }
    when 18..65
      Success(:adult) { "You are an adult" }
    else
      Success(:senior) { "You are a senior" }
    end
  end
end

AgeCategoriesOperation.call(age: 18) \
  .on_success { |_result| puts "Great, you are an adult" } \
  .on_success(:senior) { |_result| puts "Enjoy your retirement" } \
  .on_success(:adult, :senior) { |_result| puts "Allowed access" } \
  .on_failure { |_result| puts "Sorry, you are too young" } \
  .on_failure(:teenager) { |_result| puts "Almost there, you are a teenager" }
# The above example will print:
#   Great, you are an adult
#   Allowed access

Operation Flow

Once things become more complex, you can use can define a flow with a list of steps to be executed:

class ArithmeticOperation < Fluxo::Operation
  flow :normalize, :plus_one, :double, :square, :wrap

  def normalize(num:)
    Success(num: num.to_i)
  end

  def plus_one(num:)
    return Failure('cannot be zero') if num == 0

    Success(num: num + 1)
  end

  def double(num:)
    Success(num: num * 2)
  end

  def square(num:)
    Success(num: num * num)
  end

  def wrap(num:)
    Success(num)
  end
end

ArithmeticOperation.call(num: 1) \
  .on_success { |result| puts "Result: #{result.value}" }
# Result: 16

Notice that the value of each step is passed to the next step as an argument. You can include more transient attributes during the flow execution. Step result with object different of a Hash will be ignored. And the last step is always the result of the operation.

class CreateUserOperation < Fluxo::Operation
  flow :build, :save

  def build(name:, age:)
    user = User.new(name: name, age: age)
    Success(user: user)
  end

  def save(user:, **)
    return Failure(user.errors) unless user.save

    Success(user: user)
  end
end

Operation Groups

Another very useful feature of Fluxo is the ability to group operations steps. Imagine that you want to execute a bunch of operations in a single transaction. You can do this by defining a the group method and specifying the steps to be executed in the group.

class ApplicationOperation < Fluxo::Operation
  private

  def transaction(**kwargs, &block)
    ActiveRecord::Base.transaction do
      result = block.call(**kwargs)
      raise(ActiveRecord::Rollback) unless result.success?
    end
    result
  end
end

class CreateUserOperation < ApplicationOperation
  flow :build, {transaction: %i[save_user save_profile]}, :enqueue_job

  def build(name:, email:)
    user = User.new(name: name, email: email)
    Success(user: user)
  end

  def save_user(user:, **)
    return Failure(user.errors) unless user.save

    Success(user: user)
  end

  def save_profile(user:, **)
    UserProfile.create!(user: user)
    Success()
  end

  def enqueue_job(user:, **)
    UserJob.perform_later(user.id)

    Success(user)
  end
end

Operation Validation

If you have the ActiveModel gem installed, you can use the validations method to define validations on the operation.

class SubscribeOperation < Fluxo::Operation
  validations do
    validates :name, presence: true
    validates :email, presence: true, format: { with: /\A[^@]+@[^@]+\z/ }
  end

  def call!(name:, email:)
    # ...
  end
end

Operations Composition

To promote single responsibility principle, Fluxo allows compose a complex operation flow by combining other operations.

class DoubleOperation < Fluxo::Operation
  def call!(num:)
    Success(num: num * 2)
  end
end

class SquareOperation < Fluxo::Operation
  def call!(num:)
    Success(num: num * 2)
  end
end

class ArithmeticOperation < Fluxo::Operation
  flow :normalize, :double, :square

  def normalize(num:)
    Success(num: num.to_i)
  end

  def double(num:)
    DoubleOperation.call(num: num)
  end

  def square(num:)
    SquareOperation.call(num: num)
  end
end

Configuration

Fluxo.configure do |config|
  config.wrap_falsey_result = false
  config.wrap_truthy_result = false
  config.strict = true
  config.error_handlers << ->(result) { Honeybadger.notify(result.value) }
end

Development

After checking out the repo, run bin/setup to install dependencies. 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 tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/marcosgz/fluxo.

License

The gem is available as open source under the terms of the MIT License.