bogo 0.2.14 → 0.2.16
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 +4 -4
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +10 -14
- data/LICENSE +2 -2
- data/bogo.gemspec +1 -2
- data/lib/bogo/animal_strings.rb +0 -4
- data/lib/bogo/constants.rb +0 -6
- data/lib/bogo/ephemeral_file.rb +0 -3
- data/lib/bogo/http_proxy.rb +0 -2
- data/lib/bogo/lazy.rb +61 -47
- data/lib/bogo/logger.rb +5 -12
- data/lib/bogo/memoization.rb +1 -7
- data/lib/bogo/priority_queue.rb +8 -9
- data/lib/bogo/retry.rb +4 -14
- data/lib/bogo/smash.rb +5 -11
- data/lib/bogo/stack.rb +725 -0
- data/lib/bogo/utility.rb +0 -2
- data/lib/bogo/version.rb +1 -1
- data/lib/bogo.rb +1 -0
- metadata +8 -22
data/lib/bogo/stack.rb
ADDED
@@ -0,0 +1,725 @@
|
|
1
|
+
require "monitor"
|
2
|
+
|
3
|
+
module Bogo
|
4
|
+
# Simple call stack implementation
|
5
|
+
class Stack
|
6
|
+
class Hooks
|
7
|
+
include MonitorMixin
|
8
|
+
|
9
|
+
# @return [Array<Entry>] list of entries to prepend to stack actions
|
10
|
+
attr_reader :prepend_entries
|
11
|
+
# @return [Array<Entry>] list of entries to append to stack actions
|
12
|
+
attr_reader :append_entries
|
13
|
+
# @return [Array<Entry>] list of entries to prepend to specific actions
|
14
|
+
attr_reader :before_entries
|
15
|
+
# @return [Array<Entry>] list of entries to append to specific actions
|
16
|
+
attr_reader :after_entries
|
17
|
+
|
18
|
+
# @return [Stack] stack associated with these hooks
|
19
|
+
attr_reader :stack
|
20
|
+
|
21
|
+
# Create a new set hooks
|
22
|
+
#
|
23
|
+
# @param stack [Stack]
|
24
|
+
# @return [self]
|
25
|
+
def initialize(stack:)
|
26
|
+
super()
|
27
|
+
if !stack.is_a?(Stack)
|
28
|
+
raise TypeError,
|
29
|
+
"Expecting `#{Stack.name}` but received `#{stack.class.name}`"
|
30
|
+
end
|
31
|
+
@prepend_entries = [].freeze
|
32
|
+
@append_entries = [].freeze
|
33
|
+
@after_entries = [].freeze
|
34
|
+
@before_entries = [].freeze
|
35
|
+
@applied = false
|
36
|
+
@stack = stack
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add hook after identifier
|
40
|
+
#
|
41
|
+
# @param identifier [Symbol, Class, Proc] action to hook after
|
42
|
+
# @yieldblock Hook to execute
|
43
|
+
# @return [self]
|
44
|
+
def after(identifier, &block)
|
45
|
+
be_callable!(identifier) unless identifier.is_a?(Symbol)
|
46
|
+
be_callable!(block)
|
47
|
+
synchronize do
|
48
|
+
if applied?
|
49
|
+
raise Error::ApplyError,
|
50
|
+
"Hooks have already been applied to stack"
|
51
|
+
end
|
52
|
+
@after_entries = after_entries +
|
53
|
+
[Entry.new(identifier: identifier,
|
54
|
+
action: Action.new(stack: stack, callable: block))]
|
55
|
+
@after_entries.freeze
|
56
|
+
end
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add hook before identifier
|
61
|
+
#
|
62
|
+
# @param identifier [Symbol, Class, Proc] action to hook before
|
63
|
+
# @yieldblock Hook to execute
|
64
|
+
# @return [self]
|
65
|
+
def before(identifier, &block)
|
66
|
+
be_callable!(identifier) unless identifier.is_a?(Symbol)
|
67
|
+
be_callable!(block)
|
68
|
+
synchronize do
|
69
|
+
if applied?
|
70
|
+
raise Error::ApplyError,
|
71
|
+
"Hooks have already been applied to stack"
|
72
|
+
end
|
73
|
+
@before_entries = before_entries +
|
74
|
+
[Entry.new(identifier: identifier,
|
75
|
+
action: Action.new(stack: stack, callable: block))]
|
76
|
+
@before_entries.freeze
|
77
|
+
end
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
# Add hook before stack actions
|
82
|
+
#
|
83
|
+
# @yieldblock Hook to execute
|
84
|
+
# @return [self]
|
85
|
+
def prepend(&block)
|
86
|
+
be_callable!(block)
|
87
|
+
synchronize do
|
88
|
+
if applied?
|
89
|
+
raise Error::ApplyError,
|
90
|
+
"Hooks have already been applied to stack"
|
91
|
+
end
|
92
|
+
@prepend_entries = prepend_entries +
|
93
|
+
[Action.new(stack: stack, callable: block)]
|
94
|
+
@prepend_entries.freeze
|
95
|
+
end
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add hook after stack actions
|
100
|
+
#
|
101
|
+
# @yieldblock Hook to execute
|
102
|
+
# @return [self]
|
103
|
+
def append(&block)
|
104
|
+
be_callable!(block)
|
105
|
+
synchronize do
|
106
|
+
if applied?
|
107
|
+
raise Error::ApplyError,
|
108
|
+
"Hooks have already been applied to stack"
|
109
|
+
end
|
110
|
+
@append_entries = append_entries +
|
111
|
+
[Action.new(stack: stack, callable: block)]
|
112
|
+
@append_entries.freeze
|
113
|
+
end
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Boolean] hooks have been applied to stack
|
118
|
+
def applied?
|
119
|
+
!!@applied
|
120
|
+
end
|
121
|
+
|
122
|
+
# Apply hooks to stack action list
|
123
|
+
#
|
124
|
+
# @return [Array<Action>] action list with hooks
|
125
|
+
def apply!
|
126
|
+
synchronize do
|
127
|
+
if applied?
|
128
|
+
raise Error::ApplyError,
|
129
|
+
"Hooks have already been applied to stack"
|
130
|
+
end
|
131
|
+
actions = stack.actions.dup
|
132
|
+
stubs = [:stub] * actions.size
|
133
|
+
before_entries.find_all { |e| e.identifier == :all }.each do |entry|
|
134
|
+
stubs.count.times.to_a.reverse.each do |i|
|
135
|
+
stubs.insert(i, entry.action)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
after_entries.find_all { |e| e.identifier == :all }.each do |entry|
|
139
|
+
stubs.count.times.to_a.reverse.each do |i|
|
140
|
+
stubs.insert(i + 1, entry.action)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
actions = stubs.map do |item|
|
144
|
+
item == :stub ? actions.pop : item
|
145
|
+
end
|
146
|
+
before_entries.find_all { |e| e.identifier != :all }.each do |entry|
|
147
|
+
idx = actions.index { |a| a.callable == entry.identifier }
|
148
|
+
next if idx.nil?
|
149
|
+
actions.insert(idx, entry.action)
|
150
|
+
end
|
151
|
+
after_entries.find_all { |e| e.identifier != :all }.each do |entry|
|
152
|
+
idx = actions.index { |a| a.callable == entry.identifier }
|
153
|
+
next if idx.nil?
|
154
|
+
actions.insert(idx + 1, entry.action)
|
155
|
+
end
|
156
|
+
@applied = true
|
157
|
+
actions = prepend_entries + actions + append_entries
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
# Raise exception if given thing is not a callable
|
164
|
+
#
|
165
|
+
# @param thing [Object]
|
166
|
+
# @return [
|
167
|
+
def be_callable!(thing)
|
168
|
+
return if thing.respond_to?(:call)
|
169
|
+
return if thing.is_a?(Class) && thing.instance_methods.include?(:call)
|
170
|
+
raise TypeError, "Expecting callable but received `#{thing.class.name}`"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class Entry
|
175
|
+
attr_reader :identifier
|
176
|
+
attr_reader :action
|
177
|
+
|
178
|
+
def initialize(identifier:, action:)
|
179
|
+
if !action.is_a?(Action)
|
180
|
+
raise TypeError, "Expecting `#{Action.name}` but received `#{action.class.name}`"
|
181
|
+
end
|
182
|
+
@identifier = identifier
|
183
|
+
@action = action
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Stack related errors
|
188
|
+
class Error < StandardError
|
189
|
+
class PreparedError < Error; end
|
190
|
+
class UnpreparedError < Error; end
|
191
|
+
class ApplyError < Error; end
|
192
|
+
class CalledError < Error; end
|
193
|
+
class InvalidArgumentsError < Error; end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Context for the stack execution
|
197
|
+
class Context
|
198
|
+
include MonitorMixin
|
199
|
+
|
200
|
+
# @return [Array<Stack>] list of stacks associated to context
|
201
|
+
attr_reader :stacks
|
202
|
+
|
203
|
+
# Create a new context
|
204
|
+
#
|
205
|
+
# @param stack [Stack] initial stack associated to this context
|
206
|
+
# @return [Stack]
|
207
|
+
def initialize(*args, stack:)
|
208
|
+
super()
|
209
|
+
if !stack.is_a?(Stack)
|
210
|
+
raise TypeError,
|
211
|
+
"Expecting `#{Stack.name}` but received `#{stack.class.name}`"
|
212
|
+
end
|
213
|
+
@stacks = [stack].freeze
|
214
|
+
@data = Smash.new
|
215
|
+
freeze_data!
|
216
|
+
end
|
217
|
+
|
218
|
+
# Associate stack with this context
|
219
|
+
#
|
220
|
+
# @param stack [Stack]
|
221
|
+
# @return [self]
|
222
|
+
def for(stack)
|
223
|
+
@stacks = @stacks.dup.push(stack).freeze
|
224
|
+
self
|
225
|
+
end
|
226
|
+
|
227
|
+
# Check if value is set.
|
228
|
+
#
|
229
|
+
# @return [Boolean]
|
230
|
+
def is_set?(*key)
|
231
|
+
synchronize do
|
232
|
+
val = @data.get(*key)
|
233
|
+
return false if val.nil?
|
234
|
+
return false if val.is_a?(MonitorMixin::ConditionVariable)
|
235
|
+
true
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Fetch stored value from key location. If value
|
240
|
+
# is not set, will wait until value is available.
|
241
|
+
#
|
242
|
+
# @param key [String, Symbol] path to value location
|
243
|
+
# @return [Object]
|
244
|
+
def get(*key)
|
245
|
+
synchronize do
|
246
|
+
val = @data.get(*key)
|
247
|
+
return val if !val.nil?
|
248
|
+
val = new_cond
|
249
|
+
set(*key, val)
|
250
|
+
val.wait
|
251
|
+
@data.get(*key)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Fetch stored value from key location. if value
|
256
|
+
# is not set, will return nil immediately
|
257
|
+
#
|
258
|
+
# @param key [String, Symbol] path to value location
|
259
|
+
# @return [Object, nil]
|
260
|
+
def grab(*key)
|
261
|
+
synchronize do
|
262
|
+
@data.get(*key)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Store value at key location
|
267
|
+
#
|
268
|
+
# @param key [String, Symbol] path to value location
|
269
|
+
# @param value [Object] value to store
|
270
|
+
# @return [Object] value
|
271
|
+
def set(*key, value)
|
272
|
+
synchronize do
|
273
|
+
return delete(*key) if
|
274
|
+
value.nil? && !@data.get(*key).is_a?(MonitorMixin::ConditionVariable)
|
275
|
+
|
276
|
+
e_val = @data.get(*key)
|
277
|
+
new_data = @data.to_smash
|
278
|
+
new_data.set(*key, value)
|
279
|
+
@data = new_data.to_smash(:freeze).freeze
|
280
|
+
if e_val.is_a?(MonitorMixin::ConditionVariable)
|
281
|
+
e_val.broadcast
|
282
|
+
end
|
283
|
+
value
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Delete the key from the path
|
288
|
+
#
|
289
|
+
# @param path [String, Symbol] path to Hash
|
290
|
+
# @param key [String, Symbol] key to delete
|
291
|
+
# @return [Object, nil] removed value
|
292
|
+
def delete(*path, key)
|
293
|
+
synchronize do
|
294
|
+
e_val = @data.get(*path, key)
|
295
|
+
return if e_val.nil? || e_val.is_a?(MonitorMixin::ConditionVariable)
|
296
|
+
new_data = @data.to_smash
|
297
|
+
base = new_data.get(*path)
|
298
|
+
base.delete(key)
|
299
|
+
@data = new_data.to_smash(:freeze).freeze
|
300
|
+
e_val
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
protected
|
305
|
+
|
306
|
+
# Freeze the underlying data
|
307
|
+
def freeze_data!
|
308
|
+
@data = @data.to_smash(:freeze).freeze
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Actions which are run via the stack
|
313
|
+
class Action
|
314
|
+
include MonitorMixin
|
315
|
+
|
316
|
+
# Arguments to pass to action when called
|
317
|
+
class Arguments
|
318
|
+
# @return [Array<Object>] list of arguments
|
319
|
+
attr_reader :list
|
320
|
+
# @return [Hash<Symbol,Object>] named arguments
|
321
|
+
attr_reader :named
|
322
|
+
|
323
|
+
def initialize(list: [], named: {})
|
324
|
+
list = [] if list.nil?
|
325
|
+
named = {} if named.nil?
|
326
|
+
raise TypeError, "Expected Array but received #{list.class.name}" if
|
327
|
+
!list.is_a?(Array)
|
328
|
+
raise TypeError, "Expecting Hash but received #{named.class.name}" if
|
329
|
+
!named.is_a?(Hash)
|
330
|
+
@list = list
|
331
|
+
@named = Hash[named.map{ |k,v| [k.to_sym, v] }]
|
332
|
+
end
|
333
|
+
|
334
|
+
# Generate a new Arguments instance when given an argument
|
335
|
+
# list and the method they will be provided to
|
336
|
+
#
|
337
|
+
# @param callable [Method,Proc] method to call with arguments
|
338
|
+
# @param arguments [Array] arguments to call method
|
339
|
+
# @return [Arguments]
|
340
|
+
def self.load(callable:, arguments:)
|
341
|
+
arguments = arguments.dup
|
342
|
+
nargs = {}
|
343
|
+
# check if we have any named parameters
|
344
|
+
if callable.parameters.any?{ |p| [:key, :keyreq].include?(p.first) } && arguments.last.is_a?(Hash)
|
345
|
+
p_keys = callable.parameters.map{ |p| p.last if [:key, :keyreq].include?(p.first) }
|
346
|
+
e_keys = arguments.last.keys
|
347
|
+
if (e_keys - p_keys).empty? || (e_keys.map(&:to_sym) - p_keys).empty?
|
348
|
+
nargs = arguments.pop
|
349
|
+
end
|
350
|
+
end
|
351
|
+
self.new(list: arguments, named: nargs)
|
352
|
+
end
|
353
|
+
|
354
|
+
# Validate defined arguments can be properly applied
|
355
|
+
# to the given callable
|
356
|
+
#
|
357
|
+
# @param callable [Proc, Object] Instance that responds to #call or Proc
|
358
|
+
def validate!(callable)
|
359
|
+
params = callable.is_a?(Proc) ? callable.parameters :
|
360
|
+
callable.method(:call).parameters
|
361
|
+
l = list.dup
|
362
|
+
n = named.dup
|
363
|
+
params.each do |param|
|
364
|
+
type, name = param
|
365
|
+
case type
|
366
|
+
when :key
|
367
|
+
n.delete(name)
|
368
|
+
when :keyreq
|
369
|
+
if !n.key?(name)
|
370
|
+
raise Error::InvalidArgumentsError,
|
371
|
+
"Missing named argument `#{name}' for action"
|
372
|
+
end
|
373
|
+
n.delete(name)
|
374
|
+
when :keyrest
|
375
|
+
n.clear
|
376
|
+
when :rest
|
377
|
+
l.clear
|
378
|
+
when :req
|
379
|
+
if l.size < 1
|
380
|
+
raise Error::InvalidArgumentsError,
|
381
|
+
"Missing required argument `#{name}' for action"
|
382
|
+
end
|
383
|
+
l.shift
|
384
|
+
when :opt
|
385
|
+
l.shift
|
386
|
+
end
|
387
|
+
end
|
388
|
+
raise Error::InvalidArgumentsError,
|
389
|
+
"Too many arguments provided to action" if !l.empty?
|
390
|
+
if !n.empty?
|
391
|
+
keys = n.keys.map { |k| "#{k}'"}.join(", `")
|
392
|
+
raise Error::InvalidArgumentsError,
|
393
|
+
"Unknown named arguments provided to action `#{keys}'"
|
394
|
+
end
|
395
|
+
nil
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# @return [Stack] parent stack
|
400
|
+
attr_reader :stack
|
401
|
+
# @return [Object] callable
|
402
|
+
attr_reader :callable
|
403
|
+
# @return [Array<Object>, Arguments] arguments for callable
|
404
|
+
attr_reader :arguments
|
405
|
+
|
406
|
+
# Create a new action
|
407
|
+
#
|
408
|
+
# @param stack [Stack] stack associated with this action
|
409
|
+
# @param callable [Object] callable item
|
410
|
+
# @return [Action]
|
411
|
+
def initialize(stack:, callable: nil, &block)
|
412
|
+
super()
|
413
|
+
if callable && block
|
414
|
+
raise ArgumentError,
|
415
|
+
"Expecting callable argument or block, not both"
|
416
|
+
end
|
417
|
+
c = callable || block
|
418
|
+
if c.is_a?(Class)
|
419
|
+
if !c.instance_methods.include?(:call)
|
420
|
+
raise ArgumentError,
|
421
|
+
"Expecting callable but class does not provide `#call'"
|
422
|
+
end
|
423
|
+
else
|
424
|
+
if !c.respond_to?(:call)
|
425
|
+
raise ArgumentError,
|
426
|
+
"Expecting callable but no callable provided"
|
427
|
+
end
|
428
|
+
end
|
429
|
+
if !stack.is_a?(Stack)
|
430
|
+
raise TypeError,
|
431
|
+
"Expecting `#{Stack.name}` but received `#{stack.class.name}`"
|
432
|
+
end
|
433
|
+
@stack = stack
|
434
|
+
@callable = c
|
435
|
+
@called = false
|
436
|
+
@arguments = []
|
437
|
+
end
|
438
|
+
|
439
|
+
# Arguments to pass to callable
|
440
|
+
def with(*args)
|
441
|
+
synchronize do
|
442
|
+
if @arguments.frozen?
|
443
|
+
raise Error::PreparedError,
|
444
|
+
"Cannot set arguments after action has been prepared"
|
445
|
+
end
|
446
|
+
@arguments = args
|
447
|
+
end
|
448
|
+
self
|
449
|
+
end
|
450
|
+
|
451
|
+
# Prepare the action to be called
|
452
|
+
def prepare
|
453
|
+
synchronize do
|
454
|
+
if @arguments.frozen?
|
455
|
+
raise Error::PreparedError,
|
456
|
+
"Action has already been prepared"
|
457
|
+
end
|
458
|
+
if callable.is_a?(Class)
|
459
|
+
@callable = callable.new
|
460
|
+
end
|
461
|
+
if !callable.respond_to?(:call)
|
462
|
+
raise ArgumentError,
|
463
|
+
"Given callable does not respond to `#call'"
|
464
|
+
end
|
465
|
+
m = callable.method(:call)
|
466
|
+
@arguments = Arguments.load(callable: m, arguments: @arguments)
|
467
|
+
if m.parameters.any?{ |p| [:key, :keyreq].include?(p.first) && p.last == :context }
|
468
|
+
@arguments.named[:context] = stack.context if !@arguments.key?(:context)
|
469
|
+
end
|
470
|
+
@arguments.validate!(m)
|
471
|
+
@callable.freeze
|
472
|
+
@arguments.freeze
|
473
|
+
end
|
474
|
+
self
|
475
|
+
end
|
476
|
+
|
477
|
+
# @return [Boolean] action has been called
|
478
|
+
def called?
|
479
|
+
!!@called
|
480
|
+
end
|
481
|
+
|
482
|
+
# Call the action
|
483
|
+
#
|
484
|
+
# @param ctx [Context] context data
|
485
|
+
def call(context: nil)
|
486
|
+
synchronize do
|
487
|
+
raise Error::PreparedError,
|
488
|
+
"Cannot call action, not prepared" if !arguments.frozen?
|
489
|
+
raise Error::CalledError,
|
490
|
+
"Action has already been called" if called?
|
491
|
+
@called = true
|
492
|
+
callable.call(*arguments.list, **arguments.named)
|
493
|
+
end
|
494
|
+
stack.call(context: context)
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
include MonitorMixin
|
499
|
+
|
500
|
+
# @return [Array<Action>] list of actions in the stack
|
501
|
+
attr_reader :actions
|
502
|
+
# @return [Context] context for the stack
|
503
|
+
attr_reader :context
|
504
|
+
# @return [Hooks] hooks for stack
|
505
|
+
attr_reader :hooks
|
506
|
+
# @return [Boolean] actions run in parallel
|
507
|
+
attr_reader :parallel
|
508
|
+
|
509
|
+
# Create a new stack
|
510
|
+
#
|
511
|
+
# @return [Stack]
|
512
|
+
def initialize
|
513
|
+
super
|
514
|
+
@actions = [].freeze
|
515
|
+
@prepared = false
|
516
|
+
@context = Context.new(stack: self)
|
517
|
+
@hooks = Hooks.new(stack: self)
|
518
|
+
@started = false
|
519
|
+
@parallel = false
|
520
|
+
end
|
521
|
+
|
522
|
+
# Enable parallel execution of stack actions
|
523
|
+
#
|
524
|
+
# @return [self]
|
525
|
+
def parallelize!
|
526
|
+
synchronize do
|
527
|
+
be_unprepared!
|
528
|
+
@parallel = true
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Push a new callable action onto the end of the stack
|
533
|
+
#
|
534
|
+
# @param callable [Class, Proc] object that responds to #call or
|
535
|
+
# class with #call instance method
|
536
|
+
# @return [Action] generated Action instance
|
537
|
+
def push(callable=nil, &block)
|
538
|
+
synchronize do
|
539
|
+
be_unprepared!
|
540
|
+
act = Action.new(stack: self, callable: callable, &block)
|
541
|
+
@actions = ([act]+ actions).freeze
|
542
|
+
act
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
# Unshift a new callable action onto the start of the stack
|
547
|
+
#
|
548
|
+
# @param callable [Class, Proc] object that responds to #call or
|
549
|
+
# class with #call instance method
|
550
|
+
# @return [Action] generated Action instance
|
551
|
+
def unshift(callable=nil, &block)
|
552
|
+
synchronize do
|
553
|
+
be_unprepared!
|
554
|
+
act = Action.new(stack: self, callable: callable, &block)
|
555
|
+
@actions = (actions + [act]).freeze
|
556
|
+
act
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
# Remove item from the stack
|
561
|
+
#
|
562
|
+
# @param idx [Integer, Action] index or Action of Action to remove
|
563
|
+
# @yield [Array<Action>] stack content is provided to block
|
564
|
+
# @yieldreturn [Integer, Action] index or Action of Action to remove
|
565
|
+
# @return [Action, NilClass] removed entry
|
566
|
+
def remove(idx=nil)
|
567
|
+
synchronize do
|
568
|
+
be_unprepared!
|
569
|
+
idx = yield stack if idx.nil? && block_given?
|
570
|
+
if !idx.is_a?(Integer) && !idx.is_a?(Action)
|
571
|
+
raise ArgumentError,
|
572
|
+
"Expecting `Integer` or `#{Action.name}` but received `#{idx.class}`"
|
573
|
+
end
|
574
|
+
@actions = actions.dup
|
575
|
+
entry = @actions.delete(idx)
|
576
|
+
@actions.freeze
|
577
|
+
entry
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# Insert item into stack at given index
|
582
|
+
#
|
583
|
+
# @param at [Integer] index to add item
|
584
|
+
# @param callable [Class, Proc] object that responds to #call or
|
585
|
+
# class with #call instance method
|
586
|
+
# @param adjust [Integer] adjust index point
|
587
|
+
# @return [self]
|
588
|
+
def insert(at:, callable:, adjust: 0)
|
589
|
+
synchronize do
|
590
|
+
be_unprepared!
|
591
|
+
idx = yield stack if idx.nil? && block_given?
|
592
|
+
if !idx.is_a?(Integer) && !idx.is_a?(Action)
|
593
|
+
raise ArgumentError,
|
594
|
+
"Expecting `Integer` or `#{Action.name}` but received `#{idx.class.name}`"
|
595
|
+
end
|
596
|
+
callable = Action.new(stack: self, callable: callable) if
|
597
|
+
!callable.is_a?(Action)
|
598
|
+
@actions = actions.dup
|
599
|
+
@actions.insert(idx + adjust, callable)
|
600
|
+
@actions.freeze
|
601
|
+
end
|
602
|
+
self
|
603
|
+
end
|
604
|
+
alias_method :insert_at, :insert
|
605
|
+
|
606
|
+
# Insert item before given index
|
607
|
+
#
|
608
|
+
# @param idx [Integer] index to add item before
|
609
|
+
# @param callable [Class, Proc] object that responds to #call or
|
610
|
+
# class with #call instance method
|
611
|
+
# @yieldblock callable item
|
612
|
+
# @return [self]
|
613
|
+
def insert_before(idx: nil, callable: nil, &block)
|
614
|
+
insert(idx: idx, callable: callable, adjust: 1, &block)
|
615
|
+
end
|
616
|
+
|
617
|
+
# Insert item after given index
|
618
|
+
#
|
619
|
+
# @param idx [Integer] index to add item after
|
620
|
+
# @param callable [Class, Proc] object that responds to #call or
|
621
|
+
# class with #call instance method
|
622
|
+
# @yieldblock callable item
|
623
|
+
# @return [self]
|
624
|
+
def insert_after(idx: nil, callable: nil, &block)
|
625
|
+
insert(idx: idx, callable: callable, adjust: -1, &block)
|
626
|
+
end
|
627
|
+
|
628
|
+
# Remove last action from the stack
|
629
|
+
#
|
630
|
+
# @return [Action, nil]
|
631
|
+
def pop
|
632
|
+
synchronize do
|
633
|
+
@actions = actions.dup
|
634
|
+
action = actions.pop
|
635
|
+
@actions.freeze
|
636
|
+
action
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
# Remove first action from the stack
|
641
|
+
#
|
642
|
+
# @return [Action, nil]
|
643
|
+
def shift
|
644
|
+
synchronize do
|
645
|
+
be_unprepared!
|
646
|
+
@actions = actions.dup
|
647
|
+
action = actions.shift
|
648
|
+
@actions.freeze
|
649
|
+
action
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
# @return [Integer] number of actions in stack
|
654
|
+
def size
|
655
|
+
actions.size
|
656
|
+
end
|
657
|
+
|
658
|
+
# @return [TrueClass, FalseClass] stack has started execution
|
659
|
+
def started?
|
660
|
+
@started
|
661
|
+
end
|
662
|
+
|
663
|
+
# @return [TrueClass, FalseClass] stack is prepared for execution
|
664
|
+
def prepared?
|
665
|
+
@prepared
|
666
|
+
end
|
667
|
+
|
668
|
+
# Prepare the stack to be called
|
669
|
+
#
|
670
|
+
# @return [self]
|
671
|
+
def prepare
|
672
|
+
synchronize do
|
673
|
+
be_unprepared!
|
674
|
+
@actions = hooks.apply!
|
675
|
+
@actions.freeze
|
676
|
+
actions.each(&:prepare)
|
677
|
+
@prepared = true
|
678
|
+
end
|
679
|
+
self
|
680
|
+
end
|
681
|
+
|
682
|
+
# Execute the next action in the stack
|
683
|
+
#
|
684
|
+
# @param ctx [Context] start with given context
|
685
|
+
def call(context: nil)
|
686
|
+
synchronize do
|
687
|
+
be_prepared!
|
688
|
+
if context
|
689
|
+
be_unstarted!
|
690
|
+
@context = context.for(self)
|
691
|
+
end
|
692
|
+
if @parallel
|
693
|
+
acts = @actions.dup
|
694
|
+
@actions = []
|
695
|
+
acts.each do |action|
|
696
|
+
Thread.new { action.call(context: @context) }
|
697
|
+
end
|
698
|
+
else
|
699
|
+
action = pop
|
700
|
+
action.call(context: context) if action
|
701
|
+
end
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
protected
|
706
|
+
|
707
|
+
def be_unstarted!
|
708
|
+
raise Error::StartedError,
|
709
|
+
"Stack is already started and cannot be modified" if
|
710
|
+
started?
|
711
|
+
end
|
712
|
+
|
713
|
+
def be_unprepared!
|
714
|
+
raise Error::PreparedError,
|
715
|
+
"Stack is already prepared and cannot be modified" if
|
716
|
+
prepared?
|
717
|
+
end
|
718
|
+
|
719
|
+
def be_prepared!
|
720
|
+
raise Error::UnpreparedError,
|
721
|
+
"Stack must first be prepared" unless
|
722
|
+
prepared?
|
723
|
+
end
|
724
|
+
end
|
725
|
+
end
|