railway_operation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +73 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +326 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/railway_operation.rb +22 -0
- data/lib/railway_operation/generic/ensured_access.rb +42 -0
- data/lib/railway_operation/generic/filled_matrix.rb +54 -0
- data/lib/railway_operation/generic/typed_array.rb +61 -0
- data/lib/railway_operation/info.rb +187 -0
- data/lib/railway_operation/operation.rb +115 -0
- data/lib/railway_operation/operator.rb +154 -0
- data/lib/railway_operation/stepper.rb +125 -0
- data/lib/railway_operation/steps_array.rb +18 -0
- data/lib/railway_operation/strategy.rb +55 -0
- data/lib/railway_operation/surround.rb +42 -0
- data/lib/railway_operation/version.rb +5 -0
- data/railway_operation.gemspec +52 -0
- metadata +227 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deep_clone'
|
4
|
+
|
5
|
+
module RailwayOperation
|
6
|
+
module Operator
|
7
|
+
DEFAULT_STRATEGY = Strategy::DEFAULT
|
8
|
+
|
9
|
+
# The DynamicRun allows the module which includes it to have a method
|
10
|
+
# with that is run_<something>.
|
11
|
+
#
|
12
|
+
# ex: run_variation1, run_something, run_my_operation_name
|
13
|
+
module DynamicRun
|
14
|
+
CAPTURE_OPERATION_NAME = /run_*(?<operation>\w*)/
|
15
|
+
|
16
|
+
def respond_to_missing?(method, _include_private = false)
|
17
|
+
method.match(CAPTURE_OPERATION_NAME)
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(method, *args, &block)
|
21
|
+
return super unless method.match?(CAPTURE_OPERATION_NAME)
|
22
|
+
|
23
|
+
operation = method.match(CAPTURE_OPERATION_NAME)[:operation]
|
24
|
+
run(args[0], operation: operation, **(args[1] || {}))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# The operator class method allows classes which include this module
|
29
|
+
# to delegate actions to the default operation of the @operations
|
30
|
+
# array.
|
31
|
+
#
|
32
|
+
# The default operation is a normal RailwayOperation::Operation classes
|
33
|
+
# however it is used to store step declarations as well as other operation
|
34
|
+
# attributes such as track_alias, fails_step, etc. If other operations of
|
35
|
+
# the class do not declare values for these attributes, the values
|
36
|
+
# assigned to the default operation is used.
|
37
|
+
module ClassMethods
|
38
|
+
include DynamicRun
|
39
|
+
|
40
|
+
def operation(operation_or_name = :default)
|
41
|
+
@operations ||= {}
|
42
|
+
|
43
|
+
name = Operation.format_name(operation_or_name)
|
44
|
+
op = @operations[name] ||= Operation.new(operation_or_name)
|
45
|
+
|
46
|
+
# See operation/nested_operation_spec.rb for details for block syntax
|
47
|
+
|
48
|
+
block_given? ? yield(op) : op
|
49
|
+
end
|
50
|
+
|
51
|
+
def run(argument, operation: :default, **opts)
|
52
|
+
new.run(argument, operation: operation, **opts)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# The RailwayOperation::Operator instance methods exposes a single
|
57
|
+
# method - RailwayOperation::Operator#run
|
58
|
+
#
|
59
|
+
# This method is intended to run thpe default operation. Although it's
|
60
|
+
# possible to invoke ohter operations of the class the method missing
|
61
|
+
# approach is preffered (ie run_<operation_name>)
|
62
|
+
module InstanceMethods
|
63
|
+
include DynamicRun
|
64
|
+
include Surround
|
65
|
+
|
66
|
+
def run(argument, operation: :default, track_identifier: 1, step_index: 0, **info)
|
67
|
+
op = self.class.operation(operation)
|
68
|
+
|
69
|
+
wrap(*op.operation_surrounds) do
|
70
|
+
_run(
|
71
|
+
argument,
|
72
|
+
Info.new(operation: op, **info),
|
73
|
+
track_identifier: op.track_identifier(track_identifier),
|
74
|
+
step_index: step_index
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def run_step(argument, operation: :default, track_identifier:, step_index:, **info)
|
80
|
+
op = self.class.operation(operation)
|
81
|
+
|
82
|
+
new_info = Info.new(operation: op, **info)
|
83
|
+
new_info.execution.add_step(
|
84
|
+
argument: argument,
|
85
|
+
track_identifier: track_identifier,
|
86
|
+
step_index: step_index
|
87
|
+
)
|
88
|
+
|
89
|
+
_run_step(argument, new_info)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def _run_step(argument, info)
|
95
|
+
step = info.execution.last
|
96
|
+
|
97
|
+
step_definition = info.operation[step.track_identifier, step.step_index]
|
98
|
+
unless step_definition
|
99
|
+
step.noop!
|
100
|
+
return [argument, info]
|
101
|
+
end
|
102
|
+
|
103
|
+
step.start!
|
104
|
+
|
105
|
+
surrounds = info.operation.step_surrounds[step.track_identifier] + info.operation.step_surrounds['*']
|
106
|
+
wrap_arguments = [DeepClone.clone(argument), info]
|
107
|
+
|
108
|
+
step[:method] = step_definition
|
109
|
+
step[:noop] = false
|
110
|
+
|
111
|
+
step[:argument] = wrap(*surrounds, arguments: wrap_arguments) do
|
112
|
+
case step_definition
|
113
|
+
when Symbol
|
114
|
+
# add_step 1, :method
|
115
|
+
public_send(step_definition, *wrap_arguments)
|
116
|
+
when Array
|
117
|
+
# add_step 1, [MyClass, :method]
|
118
|
+
step_definition[0].send(step_definition[1], *wrap_arguments)
|
119
|
+
else
|
120
|
+
# add_step 1, ->(argument, info) { ... }
|
121
|
+
step_definition.call(*wrap_arguments)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
step.end!
|
126
|
+
|
127
|
+
[step[:argument], info]
|
128
|
+
end
|
129
|
+
|
130
|
+
def _run(argument, info, track_identifier:, step_index:)
|
131
|
+
return [argument, info] if step_index > info.operation.last_step_index
|
132
|
+
|
133
|
+
info.execution.add_step(
|
134
|
+
argument: argument,
|
135
|
+
track_identifier: track_identifier,
|
136
|
+
step_index: step_index
|
137
|
+
)
|
138
|
+
|
139
|
+
stepper_fn = info.operation.stepper_function || DEFAULT_STRATEGY
|
140
|
+
|
141
|
+
vector = Stepper.step(stepper_fn, info) do
|
142
|
+
_run_step(argument, info)
|
143
|
+
end
|
144
|
+
|
145
|
+
_run(
|
146
|
+
vector[:argument].(info),
|
147
|
+
info,
|
148
|
+
track_identifier: vector[:track_identifier].(info),
|
149
|
+
step_index: vector[:step_index].(info)
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
# This class is responsible for calculating the vector of the next step
|
5
|
+
# during an operation execution
|
6
|
+
class Stepper
|
7
|
+
module Argument
|
8
|
+
DEFAULT = ->(execution:, **) { execution.last[:argument] }
|
9
|
+
INITIAL = ->(execution:, **) { execution.first[:argument] }
|
10
|
+
PREVIOUS = ->(execution:, **) { execution[-2][:argument] }
|
11
|
+
end
|
12
|
+
|
13
|
+
module TrackIdentifier
|
14
|
+
DEFAULT = ->(execution:, **) { execution.last.track_identifier }
|
15
|
+
INITIAL = ->(operation:, **) { operation.initial_track }
|
16
|
+
NOOP = ->(operation:, **) { operation.noop_track }
|
17
|
+
end
|
18
|
+
|
19
|
+
module StepIndex
|
20
|
+
DEFAULT = ->(execution:, **) { execution.last.step_index + 1 }
|
21
|
+
INITIAL = ->(_) { 0 }
|
22
|
+
CURRENT = ->(execution:, **) { execution.last.step_index }
|
23
|
+
end
|
24
|
+
|
25
|
+
def vector
|
26
|
+
@vector ||= {
|
27
|
+
argument: Argument::DEFAULT,
|
28
|
+
step_index: StepIndex::DEFAULT,
|
29
|
+
track_identifier: TrackIdentifier::DEFAULT
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](key)
|
34
|
+
vector[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.step(*args, &block)
|
38
|
+
new.step(*args, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def step(stepper_function, info, &step_executor)
|
42
|
+
stepper_function.call(self, info, &step_executor)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Manipulators
|
47
|
+
|
48
|
+
def continue
|
49
|
+
vector
|
50
|
+
end
|
51
|
+
|
52
|
+
def switch_to(specified_track)
|
53
|
+
vector[:track_identifier] = lambda do |execution:, operation:, **|
|
54
|
+
begin
|
55
|
+
track = case specified_track
|
56
|
+
when Proc
|
57
|
+
specified_track.call(operation, execution.last.track_identifier)
|
58
|
+
else
|
59
|
+
specified_track
|
60
|
+
end
|
61
|
+
|
62
|
+
operation.track_index(track) # ensures that track index is found in the operation
|
63
|
+
track
|
64
|
+
rescue Operation::NonExistentTrack
|
65
|
+
raise "Invalid stepper_function specification for '#{operation.name}'"\
|
66
|
+
"operation: invalid `switch_to(#{track})`"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def retry_step
|
72
|
+
vector.merge!(
|
73
|
+
argument: Argument::PREVIOUS,
|
74
|
+
track_identifier: TrackIdentifier::DEFAULT
|
75
|
+
)
|
76
|
+
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def restart_operation
|
81
|
+
vector.merge!(
|
82
|
+
argument: Argument::INITIAL,
|
83
|
+
track_identifier: TrackIdentifier::INITIAL
|
84
|
+
)
|
85
|
+
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def halt_operation
|
90
|
+
vector.merge!(
|
91
|
+
argument: Argument::DEFAULT,
|
92
|
+
track_identifier: TrackIdentifier::NOOP
|
93
|
+
)
|
94
|
+
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def fail_operation
|
99
|
+
vector.merge!(
|
100
|
+
argument: Argument::INITIAL,
|
101
|
+
track_identifier: TrackIdentifier::NOOP
|
102
|
+
)
|
103
|
+
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def successor_track
|
108
|
+
lambda do |operation, current_track|
|
109
|
+
operation.successor_track(current_track)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def raise_error(e, info)
|
114
|
+
info.execution.last_step[:succeeded] = false
|
115
|
+
step_index = info.execution.length - 1
|
116
|
+
track_identifier = info.execution.last_step[:track_identifier]
|
117
|
+
|
118
|
+
message = "The operation was aborted because `#{e.class}' "\
|
119
|
+
"was raised on track #{track_identifier}, step #{step_index} of the operation."\
|
120
|
+
"\n\n#{info.display}\n\n"
|
121
|
+
|
122
|
+
raise e, message, e.backtrace
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
# Ensures that the array containing surrounds are of valid type
|
5
|
+
class StepsArray < Generic::TypedArray
|
6
|
+
def initialize(*args, **options)
|
7
|
+
types = [Symbol, Proc, String, Array]
|
8
|
+
|
9
|
+
super(
|
10
|
+
*args,
|
11
|
+
ensure_type_is: types,
|
12
|
+
error_message: 'Invalid operation surround declaration, must' \
|
13
|
+
"be of type #{types}",
|
14
|
+
**options
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
class Strategy
|
5
|
+
DEFAULT = lambda do |stepper, info, &step|
|
6
|
+
begin
|
7
|
+
_result, new_info = step.call
|
8
|
+
|
9
|
+
if new_info.execution.last.failed?
|
10
|
+
stepper.fail_operation
|
11
|
+
end
|
12
|
+
|
13
|
+
stepper.continue
|
14
|
+
rescue StandardError => e
|
15
|
+
stepper.raise_error(e, new_info || info)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.standard
|
20
|
+
tracks = [:normal, :error_track, :fail_track]
|
21
|
+
|
22
|
+
stepper_fn = Strategy.norm_exceptional(
|
23
|
+
norm: {
|
24
|
+
normal: ->(execution) { !execution.errored? },
|
25
|
+
error_track: ->(execution) { execution.errored? },
|
26
|
+
fail_track: ->(execution) { execution.failed? }
|
27
|
+
}
|
28
|
+
)
|
29
|
+
|
30
|
+
[tracks, stepper_fn]
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.norm_exceptional(norm: {}, exceptional: {})
|
34
|
+
lambda do |stepper, _, &step|
|
35
|
+
begin
|
36
|
+
_, new_info = step.call
|
37
|
+
|
38
|
+
track_switch = norm.detect do |_, predicate|
|
39
|
+
predicate.call(new_info.execution)
|
40
|
+
end.first
|
41
|
+
|
42
|
+
stepper.switch_to(track_switch) if track_switch
|
43
|
+
stepper.continue
|
44
|
+
rescue StandardError => e
|
45
|
+
track_switch = exceptional.detect do |_, predicate|
|
46
|
+
predicate.call(e)
|
47
|
+
end.first
|
48
|
+
|
49
|
+
stepper.raise_error(e, new_info || info) unless track_switch
|
50
|
+
stepper.switch_to(track_switch).continue
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
module Surround
|
5
|
+
def wrap(*surrounds, arguments: [], &body)
|
6
|
+
@body = body
|
7
|
+
@arguments = arguments
|
8
|
+
|
9
|
+
execute(surrounds)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def execute(surrounds)
|
15
|
+
surround, *rest = surrounds
|
16
|
+
result = nil
|
17
|
+
|
18
|
+
send_surround(surround, *@arguments) do
|
19
|
+
result = if rest.empty?
|
20
|
+
@body.call
|
21
|
+
else
|
22
|
+
execute(rest)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def send_surround(surround, *args)
|
30
|
+
case surround
|
31
|
+
when Symbol # wrap(with: :my_method)
|
32
|
+
send(surround, *args) { yield }
|
33
|
+
when Array # wrap(with: [MyClass, :method])
|
34
|
+
surround[0].send(surround[1], *args) { yield }
|
35
|
+
when Proc # wrap(with: -> { ... })
|
36
|
+
surround.call(-> { yield }, *args)
|
37
|
+
else # no wrap
|
38
|
+
yield(*args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'railway_operation/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'railway_operation'
|
9
|
+
spec.version = RailwayOperation::VERSION
|
10
|
+
spec.authors = ['Felix Flores']
|
11
|
+
spec.email = ['felixflores@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'This is a an implementation of the railway ' \
|
14
|
+
'oriented programming pattern in ruby.'
|
15
|
+
|
16
|
+
spec.description = 'This gem allows you to declare an execution ' \
|
17
|
+
'tree for executing a series of methods and commands.'
|
18
|
+
|
19
|
+
spec.homepage = 'https://github.com/felixflores/railway_operation/'
|
20
|
+
spec.license = 'MIT'
|
21
|
+
|
22
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set
|
23
|
+
# the 'allowed_push_host' to allow pushing to a single host or delete
|
24
|
+
# this section to allow pushing to any host.
|
25
|
+
if spec.respond_to?(:metadata)
|
26
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
27
|
+
else
|
28
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
29
|
+
'public gem pushes.'
|
30
|
+
end
|
31
|
+
|
32
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
33
|
+
f.match(%r{^(test|spec|features)/})
|
34
|
+
end
|
35
|
+
|
36
|
+
spec.bindir = 'exe'
|
37
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
38
|
+
spec.require_paths = ['lib']
|
39
|
+
|
40
|
+
spec.add_runtime_dependency 'ruby_deep_clone'
|
41
|
+
spec.add_runtime_dependency 'terminal-table'
|
42
|
+
|
43
|
+
spec.add_development_dependency 'bundler', '~> 1.14'
|
44
|
+
spec.add_development_dependency 'pry-byebug'
|
45
|
+
spec.add_development_dependency 'pry-doc'
|
46
|
+
spec.add_development_dependency 'pry-rescue'
|
47
|
+
spec.add_development_dependency 'pry-clipboard'
|
48
|
+
spec.add_development_dependency 'pry-stack_explorer'
|
49
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
50
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
51
|
+
spec.add_development_dependency 'simplecov'
|
52
|
+
end
|