bogo 0.2.14 → 0.2.16

Sign up to get free protection for your applications and to get access to all the features.
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