Fluxus

Gem Version build Maintainability

Fluxus [ˈfluːk.sus] is a simple way to bring use cases to your code. The library uses what Ruby can provide and a little taste of pure object-oriented concepts.

This library takes inspiration from the Clean Architecture concepts of use cases.

Some similarities with dry-monads and u-case can easily be perceived. This library operates to bring interoperability with them.

Installation

You can add it to your project as a dependency:

gem 'fluxus'

Usage

An use case is a set of business instructions that will be carefully followed by the runtime code and its dependencies to achieve a great purpose. The level of complexity can vary, but Fluxus is here to create readability and scope around it!

Fluxus always tries to deliver more from an object-oriented paradigm than from showing itself. This means Fluxus is more about directions than dependency. And that's why we call our definition classes as Fluxus::Object and Fluxus::SafeObject.

Creating a basic Fluxus::Object

class IsEven < Fluxus::Object
    def call!(number)
        return Failure(result: "#{number} is odd") if number.odd?

        Success(result: "#{number} is even")
    end
end

IsEven.call!(2)

Creating a basic Fluxus::SafeObject

class IsEven < Fluxus::SafeObject
    def call!(number)
        return Failure(result: "#{number} is odd") if number.odd?

        Success(result: "#{number} is even")
    end
end

IsEven.call!(2)

Differences of Object and SafeObject

While Fluxus::Object preserves the actual Ruby code autonomy to dictate the flow breakers like error management. SafeObject stays in the middle and defines safe positions by respecting the Fluxus contract and delivering the Fluxus::Results::Result interface.

This approach could be useful for some error recovery systems. Since the application will always recover from it by using the error as a dependency not a runtime flow control.


The return Fluxus::Results

The expected Fluxus contract expects a Fluxus::Results::Result interface compatible as a return value.

While Object (and SafeObject) are responsible for delivering a place to hold the actual logic and control this code runtime. The Results::Result contract will be responsible for delivering back the necessary hooks and their processed data to the application.

We natively support two kinds of Fluxus::Results::Result the Success and Failure. Using this idiom is closer to the natural conception of use cases, when they fail or are successful.

Success

Success(result: true)
Success(type: :shinning, result: true)

Failure

Failure(result: false)
Failure(type: :dusty, result: false)

The Fluxus::Results::Result contract

All results share the same (abstract) ancestor definitions which mean they are interchangeable. This brings more coherence in your code when you are handling the most crucial part of the flow, their results.

The Success and Failure public contracts could be defined by

success_object = Success(type: :ok, result: 1+1)

success_object.success? # => true
success_object.failure? # => false
success_object.unknown? # => false

success_object.type # => :ok
success_object.data # => 2

failure_object = Failure(type: :fail, result: 1-1)

failure_object.success? # => false
failure_object.failure? # => true
failure_object.unknown? # => false

failure_object.type # => :fail
failure_object.data # => 0

The result and data relationship

You already noticed the Success and Failure receiving result: but getting the data from data. In fact, inside the Fluxus::Results::Result, the actual contract handles data directly. But result is a wrapper defined by Fluxus to bring more meaning inside the use case concept.

The type importance

The type is a way to categorize the major events inside the use case. Your use case can hold a variety of Success and Failure and depend on how the business is defined different paths are taken.

The Fluxus also defines a default value to bring simplicity to small use cases. The default value is :ok for Success and :error for Failure.

Prefer using symbols for handling those types.

Results are chainable

Was described that Fluxus::Results::Result are responsible for handling the (obviously) result. This means, this also needs to control the post-use case without leaking data to the runtime void.

With this in mind, the Result implements chainable methods. You can call them hooks if you wish.

Each hook represents one of the expected states.

Fluxus::Results::Result#on_success
Fluxus::Results::Result#on_failure
Fluxus::Results::Result#on_exception

The on_exception is a contract for SafeObject, but they are basically a Failure with a specialized eye for Exception looking.

Hooks are blocks

All hooks are essentially blocks, and data is available there. This means, you can define the code routine that will handle the data, and data is immutable. Each hook handles its own version of data, and this belongs there.

Hooks respect self

All hook implementation preserves the Success or Failure instance. And this brings a powerful feature to your pipeline. Use cases can chain various conclusions.


Compiling the knowledge

Using the IsEven simple use case, let's implement a fully covered Fluxus::Object, so you can fully understand how to build your own use cases.

Basic flow

class IsEven < Fluxus::Object
    def call!(number)
        return Failure(result: "#{number} is odd") if number.odd?

        Success(result: "#{number} is even")
    end
end

def my_use_case(number)
    IsEven
        .call!(number)
        .on_success { |data| puts data << '!' }
        .on_failure { |data| puts 'Why? ' << data }
end

my_use_case(2) #=> 2 is even!
my_use_case(3) #=> Why? 3 is odd
my_use_case(nil) #=> NoMethodError

Scoped Results flow

class IsEven < Fluxus::Object
    def call!(number)
        return Failure(type: :zero, result: 'you got zero') if number.zero?
        return Failure(type: :odd, result: "#{number} is odd") if number.odd?

        Success(type: :even, result: "#{number} is even")
    end
end

def my_use_case(number)
    IsEven
        .call!(number)
        .on_success(:even) { |data| p data << '!' }
        .on_failure(:odd) { |data| p 'Why? ' << data }
        .on_failure(:zero) { |data| p data }
end

my_use_case(0) #=> you got zero
my_use_case(2) #=> 2 is even!
my_use_case(3) #=> Why? 3 is odd
my_use_case(nil) #=> NoMethodError

Safe Results flow

class IsEven < Fluxus::SafeObject
    def call!(number)
        return Failure(type: :zero, result: 'you got zero') if number.zero?
        return Failure(type: :odd, result: "#{number} is odd") if number.odd?

        Success(type: :even, result: "#{number} is even")
    end
end

def my_use_case(number)
    IsEven
        .call!(number)
        .on_success(:even) { |data| p data << '!' }
        .on_failure(:odd) { |data| p 'Why? ' << data }
        .on_failure(:zero) { |data| p data }
        .on_exception(NoMethodError) { |data| p  }
end

my_use_case(0) #=> you got zero
my_use_case(2) #=> 2 is even!
my_use_case(3) #=> Why? 3 is odd
my_use_case(nil) #=> Failure with exception: data

Contributing

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

License

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