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