Small and fast ruby framework for implementing railway-like operations.
By design it is close to
Dry::Transaction and Rust control
Flows has simple and flexible DSL for defining operations and matching results.
flows is faster than Ruby's alternatives.
flows has no production dependencies so it can be used with any framework.
Add this line to your application's Gemfile:
gem 'flows', '~> 0.5'
And then execute:
Or install it yourself as:
gem install flows
Supported Ruby versions
CI tests against last patch versions every day:
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
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.0release. Right now is under 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):
Run hooks manually:
lefthook run pre-commit lefthook run pre-push
Please, never turn off the pre-commit and pre-push hooks.
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.
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.
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:
- Ruby Reek
- 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.
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 (
- add this errors to all the errors demo (
- make sure that error is displayed correctly and follows a style of the rest of implemented errors
bin/errors is done using GLI library,
bin/errors -h to explore possibilities.
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
and another alternatives a benchmarking CLI was done:
This CLI is done using GLI, run
bin/benchmark -h to explore possibilities.
flows offers the best performance among alternatives. And this CLI
is made to simplify comparison with alternatives and keep
flows the fastest solution.
Each public API method or module must be properly documented with examples and motivation behind.
To run documentation server locally run
@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.
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%.
You must follow Conventional Commits.
Allowed prefixes since
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:
refactor - pick
Also, there is one more special prefix for release commits. Release commit
messages must look like:
v0.4.0 keep a changelog
guideline must be met.
If you adding something - provide some lines to the unreleased section of the
The project strictly follows SemVer.
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.
v0.4.0 this repo strictly follow GitHub
Flow with some additions:
- branch naming using dash:
- 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