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
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "railway_operation"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'terminal-table'
|
4
|
+
|
5
|
+
require 'railway_operation/version'
|
6
|
+
require 'railway_operation/generic/ensured_access'
|
7
|
+
require 'railway_operation/generic/filled_matrix'
|
8
|
+
require 'railway_operation/generic/typed_array'
|
9
|
+
require 'railway_operation/stepper'
|
10
|
+
require 'railway_operation/strategy'
|
11
|
+
require 'railway_operation/surround'
|
12
|
+
require 'railway_operation/steps_array'
|
13
|
+
require 'railway_operation/operation'
|
14
|
+
require 'railway_operation/operator'
|
15
|
+
require 'railway_operation/info'
|
16
|
+
|
17
|
+
module RailwayOperation
|
18
|
+
def self.included(base)
|
19
|
+
base.extend Operator::ClassMethods
|
20
|
+
base.send :include, Operator::InstanceMethods
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
module Generic
|
5
|
+
# Ensures that the default value is available for use
|
6
|
+
# The problem with normal default values is that they are returned,
|
7
|
+
# they are not part of the actually collection.
|
8
|
+
#
|
9
|
+
# hash = Hash.new { [] }
|
10
|
+
# hash['a'] == []
|
11
|
+
#
|
12
|
+
# However, if you do the following
|
13
|
+
# hash['a'] << 2
|
14
|
+
# hash == {}
|
15
|
+
# hash['a'] != 2
|
16
|
+
#
|
17
|
+
# With this you can:
|
18
|
+
#
|
19
|
+
# ensured_hash = EnsuredAccess({}) { [] }
|
20
|
+
# ensured_hash['a'] << 2
|
21
|
+
# ensured_hash == { 'a' => 2 }
|
22
|
+
class EnsuredAccess < Delegator
|
23
|
+
def initialize(obj, default = nil, &block)
|
24
|
+
@obj = obj
|
25
|
+
@default = default || block
|
26
|
+
end
|
27
|
+
|
28
|
+
def __setobj__(obj)
|
29
|
+
@obj = obj
|
30
|
+
end
|
31
|
+
|
32
|
+
def __getobj__
|
33
|
+
@obj
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](key)
|
37
|
+
@obj[key] ||= @default.respond_to?(:call) ? @default.call : @default
|
38
|
+
@obj[key]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
module Generic
|
5
|
+
class FilledMatrix
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(*rows, row_type: Array)
|
9
|
+
@row_type = row_type
|
10
|
+
@matrix = EnsuredAccess.new(@row_type.new(rows)) do
|
11
|
+
EnsuredAccess.new(@row_type.new)
|
12
|
+
end
|
13
|
+
|
14
|
+
ensure_rows_length_are_equal!
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](row_index, column_index = nil)
|
18
|
+
if column_index
|
19
|
+
@matrix.__getobj__[row_index] &&
|
20
|
+
@matrix.__getobj__[row_index][column_index]
|
21
|
+
else
|
22
|
+
@matrix.__getobj__[row_index] || EnsuredAccess.new(@row_type.new)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(row_index, column_index, entry)
|
27
|
+
@max_column_index = nil # bust the max_column_index cache
|
28
|
+
|
29
|
+
@matrix[row_index][column_index] = entry
|
30
|
+
ensure_rows_length_are_equal!
|
31
|
+
|
32
|
+
@matrix
|
33
|
+
end
|
34
|
+
|
35
|
+
def each
|
36
|
+
@matrix.each do |row|
|
37
|
+
yield row
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def max_column_index
|
42
|
+
@max_column_index ||= (@matrix.compact.max_by(&:length) || []).length - 1
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def ensure_rows_length_are_equal!
|
48
|
+
@matrix.each_with_index do |_column, index|
|
49
|
+
@matrix[index][max_column_index] ||= nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
module Generic
|
5
|
+
# Ensure that only elements of specified type(s) are accepted in the array
|
6
|
+
class TypedArray < Delegator
|
7
|
+
class UnacceptableMember < StandardError; end
|
8
|
+
DEFAULT_MESSAGE = ->(type) { "unacceptable element in array, all elements must be of type #{type}" }.freeze
|
9
|
+
|
10
|
+
def initialize(array = [], ensure_type_is:, error_message: nil)
|
11
|
+
raise(ArgumentError, 'must be initialized with an array') unless array.is_a?(Array)
|
12
|
+
|
13
|
+
@types = wrap(ensure_type_is)
|
14
|
+
@error_message = error_message || DEFAULT_MESSAGE.(ensure_type_is)
|
15
|
+
__setobj__(array)
|
16
|
+
end
|
17
|
+
|
18
|
+
def __setobj__(arr)
|
19
|
+
raise UnacceptableMember, @error_message unless array_acceptable?(arr)
|
20
|
+
@arr = arr
|
21
|
+
end
|
22
|
+
|
23
|
+
def __getobj__
|
24
|
+
@arr
|
25
|
+
end
|
26
|
+
|
27
|
+
def <<(element)
|
28
|
+
raise UnacceptableMember, @error_message unless element_acceptable?(element)
|
29
|
+
@arr << element
|
30
|
+
end
|
31
|
+
|
32
|
+
def array_acceptable?(arr)
|
33
|
+
arr&.all? { |a| element_acceptable?(a) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def element_acceptable?(element)
|
37
|
+
class_acceptable?(element) || instance_acceptable?(element)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def class_acceptable?(element)
|
43
|
+
return false unless element.is_a?(Class)
|
44
|
+
@types.detect { |type| element <= type }
|
45
|
+
end
|
46
|
+
|
47
|
+
def instance_acceptable?(element)
|
48
|
+
@types.detect { |type| element.is_a?(type) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Taken from ActiveSupport Array.wrap https://apidock.com/rails/Array/wrap/class
|
52
|
+
def wrap(object)
|
53
|
+
if object.respond_to?(:to_ary)
|
54
|
+
object.to_ary || [object]
|
55
|
+
else
|
56
|
+
[object]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
class Info < DelegateClass(Hash)
|
5
|
+
def new(maybe_obj)
|
6
|
+
maybe_obj.is_a?(Info) ? maybe_obj : super
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(operation:, **info)
|
10
|
+
super info.merge(operation: operation)
|
11
|
+
end
|
12
|
+
|
13
|
+
def operation
|
14
|
+
self[:operation]
|
15
|
+
end
|
16
|
+
|
17
|
+
def operation=(op)
|
18
|
+
self[:operation] = op
|
19
|
+
end
|
20
|
+
|
21
|
+
def execution
|
22
|
+
self[:execution] = Execution.new(self[:execution] || [])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# This is intended to extend the functionality of a normal
|
27
|
+
# hash to make it easier to inspect the log
|
28
|
+
class Execution < DelegateClass(Array)
|
29
|
+
def new(maybe_obj)
|
30
|
+
maybe_obj.is_a?(Execution) ? maybe_obj : super
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(obj = [])
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def <<(value)
|
38
|
+
super Step.new(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def []=(index, value)
|
42
|
+
super index, Step.new(value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def first_step
|
46
|
+
first
|
47
|
+
end
|
48
|
+
|
49
|
+
def last_step
|
50
|
+
last
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_error(error)
|
54
|
+
last.add_error(error)
|
55
|
+
end
|
56
|
+
|
57
|
+
def errored?
|
58
|
+
any?(&:errored?)
|
59
|
+
end
|
60
|
+
|
61
|
+
def success?
|
62
|
+
all?(&:success?)
|
63
|
+
end
|
64
|
+
|
65
|
+
def failed?
|
66
|
+
!success?
|
67
|
+
end
|
68
|
+
|
69
|
+
def completed?
|
70
|
+
all?(&:completed?)
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_step(argument:, track_identifier:, step_index:)
|
74
|
+
self << {
|
75
|
+
argument: argument,
|
76
|
+
track_identifier: track_identifier,
|
77
|
+
step_index: step_index
|
78
|
+
}
|
79
|
+
|
80
|
+
last
|
81
|
+
end
|
82
|
+
|
83
|
+
def display
|
84
|
+
table = Terminal::Table.new
|
85
|
+
table.title = 'Execution'
|
86
|
+
table.headings = ['', 'Track', 'Success', 'Method', 'Errors']
|
87
|
+
table.rows = self.map do |s|
|
88
|
+
[
|
89
|
+
s[:step_index],
|
90
|
+
s[:track_identifier],
|
91
|
+
s.success?,
|
92
|
+
s[:noop] ? '--' : (s[:method].is_a?(Proc) ? 'Proc' : s[:method]),
|
93
|
+
s[:errors]
|
94
|
+
]
|
95
|
+
end
|
96
|
+
|
97
|
+
table.to_s
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Step < DelegateClass(Hash)
|
102
|
+
def new(maybe_obj)
|
103
|
+
maybe_obj.is_a?(Step) ? maybe_obj : super
|
104
|
+
end
|
105
|
+
|
106
|
+
def initialize(obj = {})
|
107
|
+
super
|
108
|
+
end
|
109
|
+
|
110
|
+
def started_at
|
111
|
+
self[:started_at]
|
112
|
+
end
|
113
|
+
|
114
|
+
def ended_at
|
115
|
+
self[:ended_at]
|
116
|
+
end
|
117
|
+
|
118
|
+
def completed_at
|
119
|
+
ended_at
|
120
|
+
end
|
121
|
+
|
122
|
+
def started?
|
123
|
+
self[:started_at]
|
124
|
+
end
|
125
|
+
|
126
|
+
def completed?
|
127
|
+
started? && self[:ended_at]
|
128
|
+
end
|
129
|
+
|
130
|
+
def success?
|
131
|
+
errors.empty? && !self[:failed]
|
132
|
+
end
|
133
|
+
|
134
|
+
def errored?
|
135
|
+
!errors.empty?
|
136
|
+
end
|
137
|
+
|
138
|
+
def failed?
|
139
|
+
!!self[:failed]
|
140
|
+
end
|
141
|
+
|
142
|
+
def noop?
|
143
|
+
self[:noop]
|
144
|
+
end
|
145
|
+
|
146
|
+
def start!
|
147
|
+
self[:started_at] = timestamp
|
148
|
+
end
|
149
|
+
|
150
|
+
def end!
|
151
|
+
raise 'cannot complete step that has not yet started' unless started?
|
152
|
+
self[:ended_at] = timestamp
|
153
|
+
end
|
154
|
+
|
155
|
+
def fail!(error)
|
156
|
+
self[:failed_at] = timestamp
|
157
|
+
add_error(error)
|
158
|
+
end
|
159
|
+
|
160
|
+
def add_error(error)
|
161
|
+
errors << error if error
|
162
|
+
end
|
163
|
+
|
164
|
+
def noop!
|
165
|
+
self[:started_at] = self[:ended_at] = timestamp
|
166
|
+
self[:method] = nil
|
167
|
+
self[:noop] = true
|
168
|
+
end
|
169
|
+
|
170
|
+
def errors
|
171
|
+
self[:errors] ||= []
|
172
|
+
self[:errors]
|
173
|
+
end
|
174
|
+
|
175
|
+
def track_identifier
|
176
|
+
self[:track_identifier]
|
177
|
+
end
|
178
|
+
|
179
|
+
def step_index
|
180
|
+
self[:step_index]
|
181
|
+
end
|
182
|
+
|
183
|
+
def timestamp
|
184
|
+
Time.respond_to?(:current) ? Time.current : Time.now
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailwayOperation
|
4
|
+
# This is the value object that holds the information necessary to
|
5
|
+
# run an operation
|
6
|
+
class Operation
|
7
|
+
class NonExistentTrack < StandardError; end
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :name, :track_alias
|
11
|
+
attr_accessor :operation_surrounds,
|
12
|
+
:step_surrounds
|
13
|
+
|
14
|
+
def self.new(operation_or_name)
|
15
|
+
return operation_or_name if operation_or_name.is_a?(Operation)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.format_name(op_or_name)
|
20
|
+
case op_or_name
|
21
|
+
when Operation
|
22
|
+
op_or_name.name
|
23
|
+
when String, Symbol
|
24
|
+
op_or_name.to_s.gsub(/\s+/, '_').downcase.to_sym
|
25
|
+
else
|
26
|
+
raise 'invalid operation name'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(name)
|
31
|
+
@name = self.class.format_name(name)
|
32
|
+
@operation_surrounds = []
|
33
|
+
@step_surrounds = Generic::EnsuredAccess.new({}) { StepsArray.new }
|
34
|
+
@track_alias = [noop_track]
|
35
|
+
@tracks = Generic::FilledMatrix.new(row_type: StepsArray)
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](track_identifier, step_index = nil)
|
39
|
+
tracks[
|
40
|
+
track_index(track_identifier),
|
41
|
+
step_index
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
def []=(track_identifier, step_index, step)
|
46
|
+
tracks[
|
47
|
+
track_index(track_identifier),
|
48
|
+
step_index
|
49
|
+
] = step
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_step(track_identifier, method = nil, &block)
|
53
|
+
self[track_identifier, last_step_index + 1] = block || method
|
54
|
+
end
|
55
|
+
|
56
|
+
def stepper_function(fn = nil, &block)
|
57
|
+
@stepper_function ||= fn || block
|
58
|
+
end
|
59
|
+
|
60
|
+
def tracks(*names)
|
61
|
+
return @tracks if names.empty?
|
62
|
+
@track_alias = [noop_track, *names]
|
63
|
+
end
|
64
|
+
|
65
|
+
def strategy(tracks, stepper_fn)
|
66
|
+
tracks(*tracks)
|
67
|
+
stepper_function(stepper_fn)
|
68
|
+
end
|
69
|
+
|
70
|
+
def last_step_index
|
71
|
+
tracks.max_column_index
|
72
|
+
end
|
73
|
+
|
74
|
+
def successor_track(track_id)
|
75
|
+
next_index = track_index(track_id) + 1
|
76
|
+
return if tracks.count <= next_index
|
77
|
+
|
78
|
+
if track_id.is_a?(Numeric)
|
79
|
+
next_index
|
80
|
+
else
|
81
|
+
track_identifier(next_index)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def track_identifier(index_or_id)
|
86
|
+
return index_or_id unless index_or_id.is_a?(Integer)
|
87
|
+
validate_index(index_or_id)
|
88
|
+
|
89
|
+
@track_alias[index_or_id] || index_or_id
|
90
|
+
end
|
91
|
+
|
92
|
+
def track_index(track_identifier)
|
93
|
+
index = @track_alias.index(track_identifier) || track_identifier
|
94
|
+
validate_index(index)
|
95
|
+
|
96
|
+
index
|
97
|
+
end
|
98
|
+
|
99
|
+
def noop_track
|
100
|
+
:noop_track
|
101
|
+
end
|
102
|
+
|
103
|
+
def initial_track
|
104
|
+
track_identifier(1)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def validate_index(index)
|
110
|
+
unless index.is_a?(Integer) && index.positive?
|
111
|
+
raise "Invalid track `#{index}`, must be a positive integer"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|