{"id":13416455,"url":"https://github.com/apneadiving/waterfall","last_synced_at":"2025-05-15T23:03:23.705Z","repository":{"id":21500277,"uuid":"24819234","full_name":"apneadiving/waterfall","owner":"apneadiving","description":"A slice of functional programming to chain ruby services and blocks, thus providing a new approach to flow control. Make them flow!","archived":false,"fork":false,"pushed_at":"2020-03-11T18:19:09.000Z","size":124,"stargazers_count":615,"open_issues_count":3,"forks_count":15,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-05-15T23:02:31.731Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/apneadiving.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-10-05T15:49:24.000Z","updated_at":"2025-03-11T06:41:54.000Z","dependencies_parsed_at":"2022-08-21T02:20:19.996Z","dependency_job_id":null,"html_url":"https://github.com/apneadiving/waterfall","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apneadiving%2Fwaterfall","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apneadiving%2Fwaterfall/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apneadiving%2Fwaterfall/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apneadiving%2Fwaterfall/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/apneadiving","download_url":"https://codeload.github.com/apneadiving/waterfall/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254436944,"owners_count":22070946,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T21:00:59.085Z","updated_at":"2025-05-15T23:03:23.644Z","avatar_url":"https://github.com/apneadiving.png","language":"Ruby","readme":"[![Code Climate](https://codeclimate.com/github/apneadiving/waterfall/badges/gpa.svg)](https://codeclimate.com/github/apneadiving/waterfall)\n[![Test Coverage](https://codeclimate.com/github/apneadiving/waterfall/badges/coverage.svg)](https://codeclimate.com/github/apneadiving/waterfall/coverage)\n[![Build Status](https://travis-ci.org/apneadiving/waterfall.svg?branch=master)](https://travis-ci.org/apneadiving/waterfall)\n[![Gem Version](https://badge.fury.io/rb/waterfall.svg)](https://badge.fury.io/rb/waterfall)\n#### Goal\n\nChain ruby commands, and treat them like a flow, which provides a new approach to application control flow.\n\nWhen logic is complicated, waterfalls show their true power and let you write intention revealing code. Above all they excel at chaining services.\n\n#### Material\n\u003ca href=\"https://leanpub.com/the-unhappy-path\"\u003e \u003cimg align=\"left\" width=\"80\" height=\"116\" src=\"https://apneadiving.github.io/images/unhappy-path.png\"\u003e \u003c/a\u003e\nUpcoming book about failure management patterns, leveraging the gem: [The Unhappy path](https://leanpub.com/the-unhappy-path)\n\nGeneral presentation blog post there: [Chain services objects like a boss](https://medium.com/p/chain-service-objects-like-a-boss-35d0b83606ab).\n\nReach me [@apneadiving](https://twitter.com/apneadiving)\n\n\n#### Overview\n\nA waterfall object has its own flow of commands, you can chain your commands and if something wrong happens, you dam the flow which bypasses the rest of the commands.\n\nHere is a basic representation:\n- green, the flow goes on, `chain` by `chain`\n- red its bypassed and only `on_dam` blocks are executed.\n\n![Waterfall Principle](https://apneadiving.github.io/images/waterfall_principle.png)\n\n#### Example\n\n```ruby\nclass FetchUser\n  include Waterfall\n\n  def initialize(user_id)\n    @user_id = user_id\n  end\n\n  def call\n    chain { @response = HTTParty.get(\"https://jsonplaceholder.typicode.com/users/#{@user_id}\") }\n    when_falsy { @response.success? }\n      .dam { \"Error status #{@response.code}\" }\n    chain(:user) { @response.body }\n  end\nend\n```\n\nand call / chain:\n\n```ruby\nFlow.new\n    .chain(user1: :user) { FetchUser.new(1) }\n    .chain(user2: :user) { FetchUser.new(2) }\n    .chain  {|outflow| puts(outflow.user1, outflow.user2)  } # report success\n    .on_dam {|error, context| puts(error, context) }         # report error\n```\n\nWhich works like:\n\n![Waterfall Logo](http://apneadiving.github.io/images/waterfall_full_example.png)\n\n## Installation\n\nFor installation, in your gemfile:\n\n    gem 'waterfall'\n\nthen `bundle` as usual.\n\n## Waterfall mixin\n\n### Outputs\n\nEach waterfall has its own `outflow` and `error_pool`.\n\n`outflow` is an Openstruct so you can get/set its property like a hash or like a standard object.\n\n### Wiki\nWiki contains many details, please check appropriate pages:\n\n- [Predicates](https://github.com/apneadiving/waterfall/wiki/Predicates)\n- [Wf Object](https://github.com/apneadiving/waterfall/wiki/Wf-object)\n- [Testing](https://github.com/apneadiving/waterfall/wiki/Testing)\n\n### Koans (!)\nYou can try and exercise your understanding of Waterfall using the [Koans here](https://github.com/apneadiving/waterfall_koans)\n\n## Illustration of chaining\nDoing\n```ruby\nFlow.new\n    .chain(foo: :bar) { Flow.new.chain(:bar){ 1 } }\n```\n\nis the same as doing:\n\n```ruby\nFlow.new\n    .chain do |outflow, parent_waterfall|\n      unless parent_waterfall.dammed?\n        child = Wf.new.chain(:bar){ 1 }\n        if child.dammed?\n          parent_waterfall.dam(child.error_pool)\n        else\n          parent_waterfall.ouflow.foo = child.outflow.bar\n        end\n      end\n    end\n```\n\nHopefully you better get the chaining power this way.\n\n\n## Syntactic sugar\nGiven:\n```ruby\nclass MyWaterfall\n  include Waterfall\n  def call\n    self.chain { 1 }\n  end\nend\n```\nYou may have noticed that I usually write:\n\n```ruby\nFlow.new\n    .chain { MyWaterfall.new }\n```\ninstead of\n```ruby\nFlow.new\n    .chain { MyWaterfall.new.call }\n```\n\nBoth are not really the same: \n- the only source of information for the gem is the return value of the block\n- if it returns a `Waterfall`, it will apply chaining logic. If ever the waterfall was not executed yet, it will trigger `call`, hence the convention.\n- if you call your waterfall object inside the block, the return value would be whatever your `call` method returns. So the gem doesnt know there was a waterfall involved and cannot apply chaining logic... unless you ensure `self` is always returned, which is cumbersome, so it's better to avoid this\n\n\nSyntax advice\n=========\n```ruby\n# this is valid\nself\n  .chain { Service1.new }\n  .chain { Service2.new }\n\n# this is equivalent\nself.chain { Service1.new }\nself.chain { Service2.new }\n\n# this is equivalent too\nchain { Service1.new }\nchain { Service2.new }\n\n# this is invalid Ruby due to the extra line\nself\n  .chain { Service1.new }\n\n  .chain { Service2.new }\n```\n\nTips\n=========\n### Error pool\nFor the error_pool, its up to you. But using Rails, I usually include ActiveModel::Validations in my services.\n\nThus you:\n\n- have a standard way to deal with errors\n- can deal with multiple errors\n- support I18n out of the box\n- can use your model errors out of the box\n\n### Conditional Flow\nIn a service, there is one and single flow, so if you need conditionals to branch off, you can do:\n```ruby\nself.chain { Service1.new }\n\nif foo?\n  self.chain { Service2.new }\nelse\n  self.chain { Service3.new }\nend\n```\n### Halting chain\nSometimes you have a flow and you need a return value. You can use `halt_chain`, which is executed whether or not the flow is dammed. It returns what the block returns. As a consequence, it cannot be chained anymore, so it must be the last command:\n\n```ruby\nself.halt_chain do |outflow, error_pool|\n  if error_pool\n    # what you want to return on error\n  else\n    # what you want to return from the outflow\n  end\nend\n```\n\n### Rails and transactions\nI'm used to wrap every single object involving database interactions within transactions, so it can be rolled back on error.\nHere is my usual setup:\n```ruby\nmodule Waterfall\n  extend ActiveSupport::Concern\n\n  class Rollback \u003c StandardError; end\n\n  def with_transaction(\u0026block)\n    ActiveRecord::Base.transaction(requires_new: true) do\n      yield\n      on_dam do\n        raise Waterfall::Rollback\n      end\n    end\n  rescue Waterfall::Rollback\n    self\n  end\nend\n```\n\nAnd to use it:\n```ruby\nclass AuthenticateUser\n  include Waterfall\n  include ActiveModel::Validations\n\n  validates :user, presence: true\n  attr_reader :user\n\n  def initialize(email, password)\n    @email, @password = email, password\n  end\n\n  def call\n    with_transaction do\n      chain { @user = User.authenticate(@email, @password) }\n      when_falsy { valid? }\n        .dam { errors }\n      chain(:user) { user }\n    end\n  end\nend\n```\nThe huge benefit is that if you call services from services, everything will be rolled back.\n\n### Undo\n\nIf you get to dam a flow, this would trigger the `reverse_flow` method in all Services previously executed.\n\n`reverse_flow` is not executed on the service which just failed, consider the `on_dam` hook in this case.\n\nTake this as a hook to undo whatever you need to undo if things go wrong. Yet, you probably do not need to bother about databases inserts: this is the purpose of `with_transaction`.\n\n### FYI\n\n`Flow` is just an alias for the `Wf` class, so just use the one you prefer :)\n\nExamples / Presentations\n========================\n- Check the [wiki for other examples](https://github.com/apneadiving/waterfall/wiki/Refactoring-examples).\n- [Structure and chain your POROs](http://slides.com/apneadiving/structure-and-chain-your-poros).\n- [Service objects implementations](https://slides.com/apneadiving/service-objects-waterfall-rails).\n- [Handling error in Rails](https://slides.com/apneadiving/handling-error-in-ruby-rails).\n\nThanks\n=========\nHuge thanks to [robhorrigan](https://github.com/robhorrigan) for the help during infinite naming brainstorming.\n","funding_links":[],"categories":["Ruby","Abstraction","Business logic"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapneadiving%2Fwaterfall","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapneadiving%2Fwaterfall","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapneadiving%2Fwaterfall/lists"}