Class: Flows::Railway

Inherits:
Object
  • Object
show all
Extended by:
Plugin::ImplicitInit, DSL, Flows::Result::Helpers
Includes:
Flows::Result::Helpers
Defined in:
lib/flows/railway.rb,
lib/flows/railway/dsl.rb,
lib/flows/railway/step.rb,
lib/flows/railway/errors.rb,
lib/flows/railway/step_list.rb

Overview

Flows::Railway is an implementation of a Railway Programming pattern.

You may read about this pattern in the following articles:

Let's review a simple task and solve it using Railway:

  • you have to get a user by ID
  • get all user's blog posts
  • and convert it to an array of HTML-strings

In such situation, we have to implement three parts of our task and compose it into something we can call, for example, from a Rails controller. Also, the first and third steps may fail (user not found, conversion to HTML failed). And if a step failed - we have to return failure info immediately.

class RenderUserBlogPosts < Flows::Railway
  step :fetch_user
  step :get_blog_posts
  step :convert_to_html

  def fetch_user(id:)
    user = User.find_by_id(id)
    user ? ok(user: user) : err(message: "User #{id} not found")
  end

  def get_blog_posts(user:)
    ok(posts: User.posts)
  end

  def convert_to_html(posts:)
    posts_html = post.map(&:text).map do |text|
      html = convert(text)
      return err(message: "cannot convert to html: #{text}")
    end

    ok(posts_html: posts_html)
  end

  private

  # returns String or nil
  def convert(text)
    # some implementation here
  end
end

RenderUserBlogPosts.call(id: 10)
# result object returned

Let's describe how it works.

First of all you have to inherit your railway from Flows::Railway.

Then you must define list of your steps using step DSL method. Steps will be executed in the given order.

The you have to provide step implementations. It should be done by using public methods with the corresponding names. Please write your step implementations in the step definition order. It will make your railway easier to read by other engineers.

Each step should return Result Object. If Result Object is successful - next step will be called or this object becomes a railway execution result in the case of last step. If Result Object is failure - this object becomes execution result immediately.

Place all the helpers methods in the private section of the class.

To help with writing methods Flows::Result::Helpers is already included.

Railway is a very simple but not very flexible abstraction. It has a good performance and a small overhead.

Flows::Railway execution rules

  • steps execution happens from the first to the last step
  • input arguments (Railway#call(...)) becomes the input of the first step
  • each step should return Result Object (Flows::Result::Helpers already included)
  • if step returns failed result - execution stops and failed Result Object returned from Railway
  • if step returns successful result - result data becomes arguments of the following step
  • if the last step returns successful result - it becomes a result of a Railway execution

Step definitions

Two ways of step definition exist. First is by using an instance method:

step :do_something

def do_something(**arguments)
  # some implementation
  # Result Object as return value
end

Second is by using lambda:

step :do_something, ->(**arguments) { ok(some: 'data') }

Definition with lambda exists for debugging/testing purposes, it has higher priority than method implementation. Do not use lambda implementations for your business logic!

Think about Railway as about small book: you have a "table of contents" in a form of step definitions and actual "chapters" in the same order in a form of public methods. And your private methods becomes something like "appendix".

Advanced initialization

In a simple case you can just invoke YourRailway.call(..). Under the hood it works like .new.call(...), but .new part will be executed ones and memoized (Plugin::ImplicitInit included).

You can include Plugin::DependencyInjector into your Railway and in this case you will need to do .new(...).call manually.

Since:

  • 0.4.0

Defined Under Namespace

Modules: DSL Classes: Error, NoStepsError, Step, StepList

Constant Summary collapse

NODE_PREPROCESSOR =

Since:

  • 0.4.0

->(input, _, _) { [[], input.unwrap] }
NODE_POSTPROCESSOR =

Since:

  • 0.4.0

lambda do |output, context, meta|
  context[:last_step] = meta[:name]

  output
end

Constants included from DSL

DSL::SingletonVarsSetup

Instance Attribute Summary

Attributes included from Plugin::ImplicitInit

#default_instance

Attributes included from DSL

#steps

Instance Method Summary collapse

Methods included from DSL

step

Constructor Details

#initializeRailway

Returns a new instance of Railway.

Raises:

Since:

  • 0.4.0



131
132
133
134
135
136
137
138
139
140
141
# File 'lib/flows/railway.rb', line 131

def initialize
  klass = self.class
  steps = klass.steps

  raise NoStepsError, klass if steps.empty?

  @__flows_railway_flow = Flows::Flow.new(
    start_node: steps.first_step_name,
    node_map: steps.to_node_map(self)
  )
end

Instance Method Details

#call(**kwargs) ⇒ Flows::Result

Executes Railway with provided keyword arguments, returns Result Object

Returns:

Since:

  • 0.4.0



146
147
148
149
150
151
152
# File 'lib/flows/railway.rb', line 146

def call(**kwargs)
  context = {}

  @__flows_railway_flow.call(ok(**kwargs), context: context).tap do |result|
    result.meta[:last_step] = context[:last_step]
  end
end