Skip to content
andriy-baran edited this page Mar 22, 2020 · 13 revisions

Linked lists

If you have a computer science background you familiar with the concept of this data structure. It has nodes, links between them and a pointer to head element. We are going to extend the linked list concept for data flow control

Railway programming in OOP

Please see the section "Cascade wrapping with a delegation "

Cell pattern

This is a composition of objects inspired by biological cells. As you understand this very naive naming approach is just a metaphor intended to help you understand the essence of this pattern.

First of all, each cell has a membrane that separates internal and external environments. Secondly, the membrane has many inputs and outputs, that help a cell to adapt to the external environment. A cell itself is represented by an internal state and a purpose to serve. Besides, it has different strategies to achieve the purpose. Finally, a cell can give off something into an external environment and get something from it. All these characteristics give us basic design rules for the new pattern.

Rail composite

Resuming everything above we can create a class that will implement the Railway oriented programming approach in OO fashion based on Cell Pattern.

class Rail
  include SteelWheel::Composite[:controllers]
  include SteelWheel::Composite[:inputs]
  include SteelWheel::Composite[:outputs]
end

Please check rail_spec.rb for more examples

Inputs and outputs

Are the parts of the external interface, their role is an adaption to external factors by introducing correspond objects that do data preparation job before and after operation execution. The current implementation allows switching between different combinations of input/output and to set way to instantiate those objects.

class MyOperation < SteelWheel::Rail
  input :mash, init: ->(klass, value) { klass.new(value.merge(my_injection: some_value)) }
  input :array, init: ->(klass, n1, n2) { klass.new(pass: n1, log: n2) }
  output :json
  output :xml
end

The init procs will be called in the moments when the flow requires creating new instance of inputclass. Another option to specify this is during the component activation

class MyOperation < SteelWheel::Rail
  json_output init: ->(klass) { klass.new(serializer: MySerializer) } do
    # Your customizations
  end
end

To specify desired input/output methods from and to are used

MyOperation.from(:mash).to(:json)

Input gets inside via the accept method

MyOperation.from(:mash).to(:json).accept(some_data).call

The output is returned in result property of operation instance

MyOperation.from(:mash).to(:json).accept(some_data).tap(&:call).result 

Also you can use this setup

class MyOperation < SteelWheel::Rail
  from :mash
  to :json
end

Class.new(MyOperation).from(:array).to(:xml)

More exaples you can find in rail/rail_inputs_spec.rb and rail/rail_outputs_spec.rb

Callbacks

Are part of internal programming interface and provide the way to handle success/failure cases.

class MyOperation < SteelWheel::Rail
  controller :preparation

  preparation_controller do
    include ActiveMolel::Validations # Only this is supported for now 

    validate { errors.add(:field, 'missing') }
  end

  def on_failure(step)
    step #=> :preparation
    # given is a reader for the current context 
    object = given.errors
    result.text = object.to_json
  end

  def on_success
   # given is a reader for the current context 
    object = given.call
    result.text = object.to_json
  end
end

Alternative callback looks like this

class MyOperation < SteelWheel::Rail
  def on_mash_failure        # Priority 0
    # Handle this
  end

  def on_preparation_failure # Priority 0
    # Handle this
  end

  def on_failure(step)       # Priority 1

  end
end

In case when output logic requires specific procedures, this sort of callbacks should be defined on output class. If they are not defined then the operation's callbacks are triggered (chain of responsibility).

class MyOperation < SteelWheel::Rail
  json_output do
    def on_authorize_failure(given) # Priority 0
      # Handle this
    end

    def on_failure(given, step)     # Priority 1
      # Handle this
    end

    def on_success(given)           # Priority 0
      # Handle this
    end
  end
end