Skip to content
Benjamin Roth edited this page May 28, 2015 · 27 revisions

Wf object

The Wf class just includes the Waterfall module. It makes it easy to create standalone waterfalls mostly to chain actions or to chain services including Waterfall or returning a Wf object.

Basically chain statements are executed in the order they appear. But if ever the waterfall is dammed, they are skipped.

If a main waterfall chains another waterfall and the child waterfall is dammed, the main waterfall would be dammed.

The point is to be able to be able to chain an expected set of actions whenever everything works fine. And to be able to quickly stop and get the errors back whenever something wrong happens.

Waterfall mixin

Overview

The Following are equivalent:

class MyService
  include Waterfall

  def call
    self.chain{ 1 + 1 }
  end
end

Wf.new.chain{ 1 + 1 }
MyService.new.call

This illustrates one convention classes including the mixin should obey: respond to call

Advice

Using Rails, I usually include ActiveModel::Validations in my services.

Thus you:

  • have a standard way to deal with errors
  • can deal with multiple errors
  • support I18n out of the box
  • can use your model errors out of the box

Predicates

chain(name_or_mapping = nil, &block) | block signature: (outflow, waterfall)

Chain is the main predicate, what it does depends on what the block returns

# main waterfall
Wf.new
  .chain(foo: :bar) do
    # child waterfall
    Wf.new.chain(:bar){ 1 }.chain(:baz){ 2 }.chain{ 3 }
  end
when block doesnt return a waterfall

The child waterfall would have the following outflow: { bar: 1, baz: 2 }

This illustrates that when the block returns a value which is not a waterfall, it stores the returned value of the block inside the name_or_mapping key of the outflow or doesnt store it if name_or_mapping is nil.

Be aware those are equivalent:

Wf.new.chain(:foo) { 1 }
Wf.new.chain{|outflow| outflow[:foo] = 1 }
Wf.new.chain{|outflow, waterfall| waterfall.update_outflow(:foo, 1) }
when block returns a waterfall

The main waterfall would have the following outflow: { foo: 1 }

The main waterfall above receives the child waterfall as a return value of its chain block. All waterfalls have independent outflows.

If name_or_mapping is nil, the main waterfall's outflow wouldnt be affected by its child (but if the child is dammed, the parent will be dammed).

If name_or_mapping is a hash, the format must be read as { name_in_parent_waterfall: :name_from_child_waterfall}. In the above example, the child returned an outflow with a bar key which has be renamed as foo in the main one.

It may look useless, because most of the time you may not rename, but... It makes things clear. You know exactly what you expect, you know exactly that you dont expect the rest the child may provide.

when_falsy(&block) | block signature: (error_pool, waterfall)

This predicate must always be used followed with dam like:

Wf.new
  .chain(:foo) { 1 } 
  .when_falsy { true } 
   .dam { "this wouldnt be executed"  }
  .when_falsy { false } 
   .dam { "errrrr"  }
  .chain(:bar) { 2 } 
  .chain(:baz) { 3 } 
  .on_dam {|error_pool| puts error_pool  }

If the block returns a falsy value, it executes the dam block, which will store the returned value in the error_pool.

Once the waterfall is dammed, all following chain blocks are skipped (wont be executed). And all the following on_dam block would be executed.

As a result the example above would return a waterfall object having its outflow equal to { foo: 1 }. Remember, its has been dammed before bar and baz would have been set.

Its error_pool would be "errrrr" and it would be puts as a result of the on_dam

Be aware those are equivalent:

Wf.new.when_falsy{ false }.dam{ 'errrr' }
Wf.new.chain{ |outflow, waterfall| waterfall.dam('errrr') unless false }

when_truthy(&block) | block signature: (error_pool, waterfall)

Behaves the same as when_falsy except it dams when its return value is truthy

Clone this wiki locally