railway_operation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailwayOperation
4
+ VERSION = '0.1.0'
5
+ 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