Class: Flows::Railway
- Inherits:
-
Object
- Object
- Flows::Railway
- 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:
- Programming on rails: Railway Oriented Programming. It's not about Ruby on Rails.
- Railway Oriented Programming: A powerful Functional Programming pattern
- Railway Oriented Programming in Elixir with Pattern Matching on Function Level and Pipelining
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.
Defined Under Namespace
Modules: DSL Classes: Error, NoStepsError, Step, StepList
Constant Summary collapse
- NODE_PREPROCESSOR =
->(input, _, _) { [[], input.unwrap] }
- NODE_POSTPROCESSOR =
lambda do |output, context, | context[:last_step] = [:name] output end
Constants included from DSL
Instance Attribute Summary
Attributes included from Plugin::ImplicitInit
Attributes included from DSL
Instance Method Summary collapse
-
#call(**kwargs) ⇒ Flows::Result
Executes Railway with provided keyword arguments, returns Result Object.
-
#initialize ⇒ Railway
constructor
A new instance of Railway.
Methods included from DSL
Constructor Details
#initialize ⇒ Railway
Returns a new instance of Railway.
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
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.[:last_step] = context[:last_step] end end |