flows 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +1 -0
- data/.reek.yml +42 -0
- data/.rubocop.yml +20 -7
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile +0 -6
- data/Gemfile.lock +139 -74
- data/README.md +158 -364
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +47 -0
- data/bin/benchmark +73 -105
- data/bin/benchmark_cli/compare.rb +118 -0
- data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
- data/bin/benchmark_cli/compare/base.rb +45 -0
- data/bin/benchmark_cli/compare/command.rb +47 -0
- data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
- data/bin/benchmark_cli/examples.rb +23 -0
- data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
- data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
- data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
- data/bin/benchmark_cli/helpers.rb +12 -0
- data/bin/benchmark_cli/ruby.rb +15 -0
- data/bin/benchmark_cli/ruby/command.rb +38 -0
- data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
- data/bin/benchmark_cli/ruby/self_class.rb +69 -0
- data/bin/benchmark_cli/ruby/structs.rb +90 -0
- data/bin/console +1 -0
- data/bin/docserver +7 -0
- data/bin/errors +118 -0
- data/bin/errors_cli/contract_error_demo.rb +49 -0
- data/bin/errors_cli/di_error_demo.rb +38 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/oc_error_demo.rb +40 -0
- data/bin/errors_cli/railway_error_demo.rb +10 -0
- data/bin/errors_cli/result_error_demo.rb +13 -0
- data/bin/errors_cli/scp_error_demo.rb +17 -0
- data/docs/README.md +2 -186
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +25 -2
- data/forspell.dict +9 -0
- data/lefthook.yml +9 -0
- data/lib/flows.rb +11 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +41 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +25 -0
- data/lib/flows/contract/hash.rb +75 -0
- data/lib/flows/contract/hash_of.rb +70 -0
- data/lib/flows/contract/helpers.rb +22 -0
- data/lib/flows/contract/predicate.rb +34 -0
- data/lib/flows/contract/transformer.rb +50 -0
- data/lib/flows/contract/tuple.rb +70 -0
- data/lib/flows/flow.rb +75 -7
- data/lib/flows/flow/node.rb +131 -0
- data/lib/flows/flow/router.rb +25 -0
- data/lib/flows/flow/router/custom.rb +54 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +20 -0
- data/lib/flows/plugin.rb +13 -0
- data/lib/flows/plugin/dependency_injector.rb +159 -0
- data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
- data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
- data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/output_contract.rb +84 -0
- data/lib/flows/plugin/output_contract/dsl.rb +36 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -37
- data/lib/flows/railway/dsl.rb +8 -19
- data/lib/flows/railway/errors.rb +8 -12
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +188 -2
- data/lib/flows/result/do.rb +160 -16
- data/lib/flows/result/err.rb +12 -6
- data/lib/flows/result/errors.rb +29 -17
- data/lib/flows/result/helpers.rb +25 -3
- data/lib/flows/result/ok.rb +12 -6
- data/lib/flows/shared_context_pipeline.rb +216 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +46 -0
- data/lib/flows/shared_context_pipeline/track.rb +67 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
- data/lib/flows/util/prepend_to_class.rb +145 -0
- data/lib/flows/version.rb +1 -1
- metadata +233 -37
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -195
- data/bin/profile_10steps +0 -106
- data/bin/ruby_benchmarks +0 -26
- data/docs/CNAME +0 -1
- data/docs/contributing/benchmarks_profiling.md +0 -3
- data/docs/contributing/local_development.md +0 -3
- data/docs/flow/direct_usage.md +0 -3
- data/docs/flow/general_idea.md +0 -3
- data/docs/operation/basic_usage.md +0 -1
- data/docs/operation/inject_steps.md +0 -3
- data/docs/operation/lambda_steps.md +0 -3
- data/docs/operation/result_shapes.md +0 -3
- data/docs/operation/routing_tracks.md +0 -3
- data/docs/operation/wrapping_steps.md +0 -3
- data/docs/overview/performance.md +0 -336
- data/docs/railway/basic_usage.md +0 -232
- data/docs/result_objects/basic_usage.md +0 -196
- data/docs/result_objects/do_notation.md +0 -139
- data/lib/flows/implicit_build.rb +0 -16
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -55
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -93
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/railway/builder.rb +0 -68
- data/lib/flows/railway/executor.rb +0 -23
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
@@ -0,0 +1,36 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module OutputContract
|
4
|
+
# DSL for OutputContract plugin.
|
5
|
+
module DSL
|
6
|
+
# Hash of contracts for successful results.
|
7
|
+
attr_reader :success_contracts
|
8
|
+
|
9
|
+
# Hash of contracts for failure results.
|
10
|
+
attr_reader :failure_contracts
|
11
|
+
|
12
|
+
Flows::Util::InheritableSingletonVars::DupStrategy.call(
|
13
|
+
self,
|
14
|
+
'@success_contracts' => {},
|
15
|
+
'@failure_contracts' => {}
|
16
|
+
)
|
17
|
+
|
18
|
+
# Defines a contract for a successful result with specific status.
|
19
|
+
#
|
20
|
+
# @param status [Symbol] Corresponding result status.
|
21
|
+
# @param contract_block [Proc] This block will be passed to {Contract.make} to get a contract.
|
22
|
+
def success_with(status, &contract_block)
|
23
|
+
success_contracts[status] = Flows::Contract.make(&contract_block)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Defines a contract for a failure result with specific status.
|
27
|
+
#
|
28
|
+
# @param status [Symbol] Corresponding result status.
|
29
|
+
# @param contract_block [Proc] This block will be passed to {Contract.make} to get a contract.
|
30
|
+
def failure_with(status, &contract_block)
|
31
|
+
failure_contracts[status] = Flows::Contract.make(&contract_block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module OutputContract
|
4
|
+
# Base error class for output contract errors.
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Raised when no single contract for successful results is defined
|
8
|
+
class NoContractError < Error
|
9
|
+
def initialize(klass)
|
10
|
+
@klass = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
"No single success contract defined for #{@klass}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Raised when result's data violates contract
|
19
|
+
class ContractError < Error
|
20
|
+
def initialize(klass, result, error)
|
21
|
+
@klass = klass
|
22
|
+
@result = result
|
23
|
+
@error = error
|
24
|
+
end
|
25
|
+
|
26
|
+
def message
|
27
|
+
shifted_error = @error.split("\n").map { |str| ' ' + str }.join("\n")
|
28
|
+
|
29
|
+
"Output contract for #{@klass} is violated.\n" \
|
30
|
+
"Result:\n" \
|
31
|
+
" `#{@result.inspect}`\n" \
|
32
|
+
"Contract Error:\n" \
|
33
|
+
"#{shifted_error}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Raised when no contract found for result
|
38
|
+
class StatusError < Error
|
39
|
+
def initialize(klass, result, allowed_statuses)
|
40
|
+
@klass = klass
|
41
|
+
@result = result
|
42
|
+
@allowed_statuses = allowed_statuses
|
43
|
+
end
|
44
|
+
|
45
|
+
def message
|
46
|
+
allowed_statuses_str = @allowed_statuses.map { |st| "`#{st.inspect}`" }.join(', ')
|
47
|
+
|
48
|
+
"Output contract for #{@klass} is violated.\n" \
|
49
|
+
"Result:\n" \
|
50
|
+
" `#{@result.inspect}`\n" \
|
51
|
+
"Contract Error:\n" \
|
52
|
+
" has unexpected status `#{@result.status.inspect}`\n" \
|
53
|
+
" allowed statuses for `#{@result.class}` are: #{allowed_statuses_str}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Raised when not a result object returned
|
58
|
+
class ResultTypeError < Error
|
59
|
+
def initialize(klass, result)
|
60
|
+
@klass = klass
|
61
|
+
@result = result
|
62
|
+
end
|
63
|
+
|
64
|
+
def message
|
65
|
+
"Output contract for #{@klass} is violated.\n" \
|
66
|
+
"Result:\n" \
|
67
|
+
" `#{@result.inspect}`\n" \
|
68
|
+
"Contract Error:\n" \
|
69
|
+
' result must be instance of `Flows::Result`'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
module OutputContract
|
4
|
+
# Contains wrappers for initializer and `#call` methods.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module Wrapper
|
8
|
+
def initialize(*args, &block)
|
9
|
+
super(*args, &block)
|
10
|
+
klass = self.class
|
11
|
+
raise NoContractError, klass if klass.success_contracts.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(*args, &block)
|
15
|
+
result = super(*args, &block)
|
16
|
+
klass = self.class
|
17
|
+
|
18
|
+
contract = Util.contract_for(klass, result)
|
19
|
+
|
20
|
+
Util.transform_result(klass, contract, result)
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
# Helper methods for {Wrapper} are extracted to this
|
26
|
+
# module as singleton methods to not pollute user classes.
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
module Util
|
30
|
+
class << self
|
31
|
+
def contract_for(klass, result)
|
32
|
+
raise ResultTypeError.new(klass, result) unless result.is_a?(Flows::Result)
|
33
|
+
|
34
|
+
status = result.status
|
35
|
+
contracts = result.ok? ? klass.success_contracts : klass.failure_contracts
|
36
|
+
|
37
|
+
contracts[status] || raise(StatusError.new(klass, result, contracts.keys))
|
38
|
+
end
|
39
|
+
|
40
|
+
def transform_result(klass, contract, result)
|
41
|
+
data = result.send(:data)
|
42
|
+
|
43
|
+
transformed_result = contract.transform(data)
|
44
|
+
raise ContractError.new(klass, result, transformed_result.error) if transformed_result.err?
|
45
|
+
|
46
|
+
result.send(:'data=', transformed_result.unwrap)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/flows/railway.rb
CHANGED
@@ -1,51 +1,154 @@
|
|
1
|
-
require_relative '
|
2
|
-
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
5
|
-
require_relative './railway/executor'
|
6
|
-
|
7
|
-
require_relative './implicit_build'
|
1
|
+
require_relative 'railway/errors'
|
2
|
+
require_relative 'railway/step'
|
3
|
+
require_relative 'railway/step_list'
|
4
|
+
require_relative 'railway/dsl'
|
8
5
|
|
9
6
|
module Flows
|
10
|
-
# Railway
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
7
|
+
# Flows::Railway is an implementation of a Railway Programming pattern.
|
8
|
+
#
|
9
|
+
# You may read about this pattern in the following articles:
|
10
|
+
#
|
11
|
+
# * [Programming on rails: Railway Oriented Programming](http://sandordargo.com/blog/2017/09/27/railway_oriented_programming).
|
12
|
+
# It's not about Ruby on Rails.
|
13
|
+
# * [Railway Oriented Programming: A powerful Functional Programming pattern](https://medium.com/@naveenkumarmuguda/railway-oriented-programming-a-powerful-functional-programming-pattern-ab454e467f31)
|
14
|
+
# * [Railway Oriented Programming in Elixir with Pattern Matching on Function Level and Pipelining](https://medium.com/elixirlabs/railway-oriented-programming-in-elixir-with-pattern-matching-on-function-level-and-pipelining-e53972cede98)
|
15
|
+
#
|
16
|
+
# Let's review a simple task and solve it using {Flows::Railway}:
|
17
|
+
#
|
18
|
+
# * you have to get a user by ID
|
19
|
+
# * get all user's blog posts
|
20
|
+
# * and convert it to an array of HTML-strings
|
21
|
+
#
|
22
|
+
# In such situation, we have to implement three parts of our task and compose it into something we can call,
|
23
|
+
# for example, from a Rails controller.
|
24
|
+
# Also, the first and third steps may fail (user not found, conversion to HTML failed).
|
25
|
+
# And if a step failed - we have to return failure info immediately.
|
26
|
+
#
|
27
|
+
# class RenderUserBlogPosts < Flows::Railway
|
28
|
+
# step :fetch_user
|
29
|
+
# step :get_blog_posts
|
30
|
+
# step :convert_to_html
|
31
|
+
#
|
32
|
+
# def fetch_user(id:)
|
33
|
+
# user = User.find_by_id(id)
|
34
|
+
# user ? ok(user: user) : err(message: "User #{id} not found")
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def get_blog_posts(user:)
|
38
|
+
# ok(posts: User.posts)
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# def convert_to_html(posts:)
|
42
|
+
# posts_html = post.map(&:text).map do |text|
|
43
|
+
# html = convert(text)
|
44
|
+
# return err(message: "cannot convert to html: #{text}")
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# ok(posts_html: posts_html)
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# private
|
51
|
+
#
|
52
|
+
# # returns String or nil
|
53
|
+
# def convert(text)
|
54
|
+
# # some implementation here
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# RenderUserBlogPosts.call(id: 10)
|
59
|
+
# # result object returned
|
60
|
+
#
|
61
|
+
# Let's describe how it works.
|
62
|
+
#
|
63
|
+
# First of all you have to inherit your railway from `Flows::Railway`.
|
64
|
+
#
|
65
|
+
# Then you must define list of your steps using `step` DSL method.
|
66
|
+
# Steps will be executed in the given order.
|
67
|
+
#
|
68
|
+
# The you have to provide step implementations. It should be done by using
|
69
|
+
# public methods with the corresponding names.
|
70
|
+
# _Please write your step implementations in the step definition order._
|
71
|
+
# _It will make your railway easier to read by other engineers._
|
72
|
+
#
|
73
|
+
# Each step should return {Flows::Result} Object.
|
74
|
+
# If Result Object is successful - next step will be called or
|
75
|
+
# this object becomes a railway execution result in the case of last step.
|
76
|
+
# If Result Object is failure - this object becomes execution result immediately.
|
77
|
+
#
|
78
|
+
# Place all the helpers methods in the private section of the class.
|
79
|
+
#
|
80
|
+
# To help with writing methods {Flows::Result::Helpers} is already included.
|
81
|
+
#
|
82
|
+
# {Railway} is a very simple but not very flexible abstraction.
|
83
|
+
# It has a good performance and a small overhead.
|
84
|
+
#
|
85
|
+
# ## `Flows::Railway` execution rules
|
86
|
+
#
|
87
|
+
# * steps execution happens from the first to the last step
|
88
|
+
# * input arguments (`Railway#call(...)`) becomes the input of the first step
|
89
|
+
# * each step should return Result Object (`Flows::Result::Helpers` already included)
|
90
|
+
# * if step returns failed result - execution stops and failed Result Object returned from Railway
|
91
|
+
# * if step returns successful result - result data becomes arguments of the following step
|
92
|
+
# * if the last step returns successful result - it becomes a result of a Railway execution
|
93
|
+
#
|
94
|
+
# ## Step definitions
|
95
|
+
#
|
96
|
+
# Two ways of step definition exist. First is by using an instance method:
|
97
|
+
#
|
98
|
+
# step :do_something
|
99
|
+
#
|
100
|
+
# def do_something(**arguments)
|
101
|
+
# # some implementation
|
102
|
+
# # Result Object as return value
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# Second is by using lambda:
|
106
|
+
#
|
107
|
+
# step :do_something, ->(**arguments) { ok(some: 'data') }
|
108
|
+
#
|
109
|
+
# Definition with lambda exists for debugging/testing purposes, it has higher priority than method implementation.
|
110
|
+
# _Do not use lambda implementations for your business logic!_
|
111
|
+
#
|
112
|
+
# __Think about Railway as about small book: you have a "table of contents"
|
113
|
+
# in a form of step definitions and actual "chapters" in the same order
|
114
|
+
# in a form of public methods. And your private methods becomes something like "appendix".__
|
115
|
+
#
|
116
|
+
# ## Advanced initialization
|
117
|
+
#
|
118
|
+
# In a simple case you can just invoke `YourRailway.call(..)`. Under the hood it works like `.new.call(...)`,
|
119
|
+
# but `.new` part will be executed ones and memoized ({Flows::Plugin::ImplicitInit} included).
|
120
|
+
#
|
121
|
+
# You can include {Flows::Plugin::DependencyInjector} into your Railway and in this case you will
|
122
|
+
# need to do `.new(...).call` manually.
|
123
|
+
class Railway
|
124
|
+
extend ::Flows::Plugin::ImplicitInit
|
16
125
|
|
17
126
|
include ::Flows::Result::Helpers
|
127
|
+
extend ::Flows::Result::Helpers
|
18
128
|
|
19
|
-
|
20
|
-
_flows_do_checks
|
129
|
+
extend DSL
|
21
130
|
|
22
|
-
|
23
|
-
|
24
|
-
|
131
|
+
def initialize
|
132
|
+
klass = self.class
|
133
|
+
steps = klass.steps
|
25
134
|
|
26
|
-
|
27
|
-
@_flows_executor.call(**params)
|
28
|
-
end
|
135
|
+
raise NoStepsError, klass if steps.empty?
|
29
136
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
137
|
+
@__flows_railway_flow = Flows::Flow.new(
|
138
|
+
start_node: steps.first_step_name,
|
139
|
+
node_map: steps.to_node_map(self)
|
140
|
+
)
|
34
141
|
end
|
35
142
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
).call
|
42
|
-
end
|
143
|
+
# Executes Railway with provided keyword arguments, returns Result Object
|
144
|
+
#
|
145
|
+
# @return [Flows::Result]
|
146
|
+
def call(**kwargs)
|
147
|
+
context = {}
|
43
148
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
class_name: self.class.name
|
48
|
-
)
|
149
|
+
@__flows_railway_flow.call(ok(**kwargs), context: context).tap do |result|
|
150
|
+
result.meta[:last_step] = context[:last_step]
|
151
|
+
end
|
49
152
|
end
|
50
153
|
end
|
51
154
|
end
|
data/lib/flows/railway/dsl.rb
CHANGED
@@ -1,27 +1,16 @@
|
|
1
1
|
module Flows
|
2
|
-
|
3
|
-
#
|
2
|
+
class Railway
|
3
|
+
# @api private
|
4
4
|
module DSL
|
5
5
|
attr_reader :steps
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
Flows::Util::InheritableSingletonVars::DupStrategy.call(
|
8
|
+
self,
|
9
|
+
'@steps' => StepList.new
|
10
|
+
)
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
::Flows::Railway::DSL.extended(subclass, steps.map(&:dup))
|
13
|
-
super
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
include Flows::Result::Helpers
|
19
|
-
|
20
|
-
def step(name, custom_body = nil)
|
21
|
-
@steps << {
|
22
|
-
name: name,
|
23
|
-
custom_body: custom_body
|
24
|
-
}
|
12
|
+
def step(name, lambda = nil)
|
13
|
+
steps.add(name: name, lambda: lambda)
|
25
14
|
end
|
26
15
|
end
|
27
16
|
end
|
data/lib/flows/railway/errors.rb
CHANGED
@@ -1,21 +1,17 @@
|
|
1
1
|
module Flows
|
2
|
-
|
3
|
-
#
|
4
|
-
class
|
5
|
-
def message
|
6
|
-
'No steps defined'
|
7
|
-
end
|
8
|
-
end
|
2
|
+
class Railway
|
3
|
+
# Base class for Railway errors
|
4
|
+
class Error < StandardError; end
|
9
5
|
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
# Raised when initializing Railway with no steps
|
7
|
+
class NoStepsError < Error
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
13
10
|
end
|
14
11
|
|
15
12
|
def message
|
16
|
-
"
|
13
|
+
"No steps defined for #{@klass}"
|
17
14
|
end
|
18
15
|
end
|
19
|
-
# rubocop:enable Style/Documentation
|
20
16
|
end
|
21
17
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Flows
|
2
|
+
class Railway
|
3
|
+
# @api private
|
4
|
+
Step = Struct.new(:name, :lambda, :next_step, keyword_init: true) do
|
5
|
+
NODE_PREPROCESSOR = ->(input, _, _) { [[], input.unwrap] }
|
6
|
+
|
7
|
+
NODE_POSTPROCESSOR = lambda do |output, context, meta|
|
8
|
+
context[:last_step] = meta[:name]
|
9
|
+
|
10
|
+
output
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_node(method_source)
|
14
|
+
Flows::Flow::Node.new(
|
15
|
+
body: lambda || method_source.method(name),
|
16
|
+
router: Flows::Flow::Router::Simple.new(next_step || :end, :end),
|
17
|
+
meta: { name: name },
|
18
|
+
preprocessor: NODE_PREPROCESSOR,
|
19
|
+
postprocessor: NODE_POSTPROCESSOR
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|