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.
@@ -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