Flows

Build Status codecov Gem Version

Small and fast ruby framework for implementing railway-like operations. By design it is close to Trailblazer::Operation, Dry::Transaction and Rust control flow style. Flows has simple and flexible DSL for defining operations and matching results. Also flows is faster than Ruby's alternatives.

flows has no production dependencies so it can be used with any framework.

Installation

Add this line to your application's Gemfile:

gem 'flows', '~> 0.5'

And then execute:

bundle

Or install it yourself as:

gem install flows

Supported Ruby versions

CI tests against last patch versions every day:

  • MRI 2.5.x
  • MRI 2.6.x

MRI 2.7.x will be added later, right now (2.7.1) this version of MRI Ruby is too unstable and produce segmentation faults inside RSpec internals.

Usage & Documentation

  • YARD documentation - this link is for master branch. You can also find YARD documentation for any released version after v0.4.0. This documentation has a lot of examples, describes motivation behind each abstraction, but lacks some guides and defined conventions.
  • Guides - guides, conventions, integration and migration notes. Will be done before v1.0.0 release. Right now is under development.

Development

Flows is designed to be framework for your business logic. It is a big responsibility. That's why flows has near to be sadistic development conventions and linter setup.

Anyone can make Flows even better

If you see some typos or unclear things in documentation or code - feel free to open an issue. Even if you don't have plans to implement a solution - a problem reporting will help development much. We cannot fix what we don't know.

Lefthook as a git hook manager

Installation on MacOS via Homebrew:

brew install Arkweid/lefthook/lefthook

Activation (in the root of the repo):

lefthook install

Run hooks manually:

lefthook run pre-commit
lefthook run pre-push

Please, never turn off the pre-commit and pre-push hooks.

Rubocop linter

Rubocop in this setup is responsible for:

  • defining code style (indentation, etc.)
  • suggest performance improvements (rubocop-performance)
  • forces all that stuff (with some exceptions) to snippets in Markdown files (rubocop-md)
  • forces unit-testing best practices (rubocop-rspec)

Rubocop config for library and RSpec files should be close to standard one only with minor amount of exceptions.

Code in Markdown snippets and /bin folder can ignore more rules. /bin folder contains only development-related scripts and tools so it's ok to ease linter requirements.

Rubocop Metrics (ABC-size, method/class length, etc) must not be eased globally. Never.

Reek linter

Ruby Reek is a very aggressive linter that forces you to do a clean OOP design.

You will be tempted to just shut up this linter many times. But believe me, in 9 of 10 cases it worth to refactor. And after each such refactoring you will understand OOP design better and better.

Rest of the linters

  • MDL - for consistent format of Markdown files
  • forspell - for spellchecking in comments and markdown files
  • inch - for documentation coverage suggestions (the only optional linter)

Default Rake task and CI

Default rake task (bundle exec rake) executes the following checks:

  • Rubocop
  • Ruby Reek
  • RSpec
  • Spellcheck (forspell)
  • MarkdownLint (mdl)

CI is also performing default Rake task. So, if you want to reproduce CI error locally - just run bundle exec rake.

Default Rake task is also executed as a pre-push git hook.

Error reporting

I hope no one will argue that clear errors makes development noticeably faster. That's why each exception in flows should be clear and easy to read.

This cannot be tested automatically: you only can test correctness automatically, convenience can only be tested manually. That's why when you introduce any new raise you have to:

  • make an error message clear and descriptive
  • add this error to errors demo CLI (bin/errors)
  • add this errors to all the errors demo (bin/all_the_errors)
  • make sure that error is displayed correctly and follows a style of the rest of implemented errors

bin/errors is done using GLI library, run bin/errors -h to explore possibilities.

Performance

Ruby is slow. Moreover, Ruby is very slow. Yes, again. In the past time we had to compare Ruby with Python. Python was faster and that's why people started to complain about Ruby performance. That was fixed. But is Ruby fast nowadays? No. Because languages like Clojure, Go, Rust, Elixir appeared and in comparison with any of these languages Ruby is very very slow.

That's why you must be extra careful with performance. Some business operations can be executed hundreds or even thousands times per request. Each line of code in your abstraction will slow down such request a bit. That's why you should think about each line performance.

Also, it's nearly impossible to make zero-cost abstractions in Ruby. The best thing you can do - to offload calculations to a class loading or initialization step. Sacrifice some warm-up time to make runtime performance better.

And to compare performance overhead between different flows abstractions and another alternatives a benchmarking CLI was done: bin/benchmark.

This CLI is done using GLI, run bin/benchmark -h to explore possibilities.

So far, flows offers the best performance among alternatives. And this CLI is made to simplify comparison with alternatives and keep flows the fastest solution.

Documentation

Each public API method or module must be properly documented with examples and motivation behind.

To run documentation server locally run bin/docserver.

Respect @since YARD documentation tag. When some module, class or method has any API change - you have to provide correct @since tag value to the documentation.

Documentation Driven Development

When you about to do some work, the following guideline can lead to the best results:

  • first, write needed class and method structure without implementation
  • write YARD documentation with motivation and usage examples for each public class, method, module.
  • write unit tests, check that tests are failing
  • write implementation until tests are green

Yes, it's TDD approach with documentation step prepended.

Unit test

Each public API method or module must be properly tested. Internal modules can be tested indirectly through public API.

Test coverage must be higher than 95%.

Commit naming

You must follow Conventional Commits.

Allowed prefixes since v0.4.0:

  • feat: - for new features
  • fix: - for bugfixes
  • perf: - for performance improvements
  • refactor: - for refactoring work
  • ci: - updates for CI configuration
  • docs: - for documentation update

Sometimes commit can have several responsibilities. As example: when you write documentation, test and implementation for a feature in the one commit. You can do extra effort to split and rearrange commits to make it atomic. But does it really provide significant value if we already have a strong convention for changelog (see the next section)?

So, when you in such situation use the first applicable prefix in the list: between docs and refactor - pick refactor.

Also, there is one more special prefix for release commits. Release commit messages must look like: release: v0.4.0.

Changelog

Starting from v0.4.0 keep a changelog guideline must be met.

If you adding something - provide some lines to the unreleased section of the CHANGELOG.md.

Versioning

The project strictly follows SemVer.

After v1.0.0 even smallest backward incompatible change will bump major version. No exceptions.

Commit with a version bump should contain only version bump and CHANGELOG.md update.

GitHub Flow

Since v0.4.0 this repo strictly follow GitHub Flow with some additions:

  • branch naming using dash: improved-contexts
  • use references to issues in commit messages and make links to issues in CHANGELOG.md

Planned features for v1.0.0

  • validation framework
  • error reporting improvements
  • various plugins for SCP (tracing, benchmarking, logging, etc)
  • site with guides and conventions