ruote 2.1.10 → 2.1.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/CHANGELOG.txt +51 -1
  2. data/CREDITS.txt +9 -0
  3. data/README.rdoc +13 -0
  4. data/Rakefile +50 -21
  5. data/TODO.txt +42 -4
  6. data/examples/pong.rb +37 -0
  7. data/lib/ruote/context.rb +19 -9
  8. data/lib/ruote/engine/process_error.rb +10 -0
  9. data/lib/ruote/engine/process_status.rb +140 -41
  10. data/lib/ruote/engine.rb +394 -27
  11. data/lib/ruote/exp/command.rb +2 -0
  12. data/lib/ruote/exp/fe_concurrence.rb +8 -0
  13. data/lib/ruote/exp/fe_concurrent_iterator.rb +3 -0
  14. data/lib/ruote/exp/fe_cursor.rb +48 -4
  15. data/lib/ruote/exp/fe_iterator.rb +40 -0
  16. data/lib/ruote/exp/fe_listen.rb +3 -3
  17. data/lib/ruote/exp/fe_participant.rb +30 -12
  18. data/lib/ruote/exp/fe_ref.rb +126 -0
  19. data/lib/ruote/exp/fe_subprocess.rb +20 -1
  20. data/lib/ruote/exp/fe_wait.rb +4 -1
  21. data/lib/ruote/exp/fe_when.rb +7 -10
  22. data/lib/ruote/exp/flowexpression.rb +23 -12
  23. data/lib/ruote/exp/ro_attributes.rb +5 -8
  24. data/lib/ruote/exp/ro_variables.rb +4 -2
  25. data/lib/ruote/fei.rb +2 -0
  26. data/lib/ruote/id/wfid_generator.rb +1 -1
  27. data/lib/ruote/log/pretty.rb +137 -0
  28. data/lib/ruote/log/storage_history.rb +1 -1
  29. data/lib/ruote/log/test_logger.rb +51 -126
  30. data/lib/ruote/log/wait_logger.rb +8 -13
  31. data/lib/ruote/parser/ruby_dsl.rb +4 -4
  32. data/lib/ruote/parser.rb +2 -2
  33. data/lib/ruote/part/block_participant.rb +1 -1
  34. data/lib/ruote/part/engine_participant.rb +1 -1
  35. data/lib/ruote/part/storage_participant.rb +27 -28
  36. data/lib/ruote/part/template.rb +8 -3
  37. data/lib/ruote/receiver/base.rb +24 -6
  38. data/lib/ruote/storage/base.rb +76 -11
  39. data/lib/ruote/storage/fs_storage.rb +10 -0
  40. data/lib/ruote/storage/hash_storage.rb +19 -8
  41. data/lib/ruote/{part → svc}/dispatch_pool.rb +3 -2
  42. data/lib/ruote/svc/dollar_sub.rb +265 -0
  43. data/lib/ruote/{error_handler.rb → svc/error_handler.rb} +6 -1
  44. data/lib/ruote/{exp → svc}/expression_map.rb +31 -37
  45. data/lib/ruote/{part → svc}/participant_list.rb +165 -25
  46. data/lib/ruote/{evt → svc}/tracker.rb +0 -0
  47. data/lib/ruote/{util → svc}/treechecker.rb +0 -0
  48. data/lib/ruote/util/look.rb +4 -1
  49. data/lib/ruote/util/ometa.rb +21 -5
  50. data/lib/ruote/{subprocess.rb → util/subprocess.rb} +0 -0
  51. data/lib/ruote/version.rb +1 -1
  52. data/lib/ruote/worker.rb +29 -69
  53. data/lib/ruote/workitem.rb +28 -1
  54. data/ruote.gemspec +26 -22
  55. data/test/functional/base.rb +3 -0
  56. data/test/functional/concurrent_base.rb +1 -0
  57. data/test/functional/crunner.sh +1 -1
  58. data/test/functional/ct_0_concurrence.rb +6 -0
  59. data/test/functional/ct_1_iterator.rb +3 -0
  60. data/test/functional/ct_2_cancel.rb +5 -0
  61. data/test/functional/eft_13_iterator.rb +39 -4
  62. data/test/functional/eft_14_cursor.rb +39 -0
  63. data/test/functional/eft_30_ref.rb +140 -0
  64. data/test/functional/eft_3_participant.rb +25 -23
  65. data/test/functional/ft_10_dollar.rb +17 -1
  66. data/test/functional/ft_14_re_apply.rb +76 -0
  67. data/test/functional/ft_1_process_status.rb +170 -29
  68. data/test/functional/ft_20_storage_participant.rb +14 -0
  69. data/test/functional/ft_24_block_participants.rb +1 -1
  70. data/test/functional/ft_26_participant_timeout.rb +93 -0
  71. data/test/functional/ft_2_errors.rb +24 -17
  72. data/test/functional/ft_30_smtp_participant.rb +7 -2
  73. data/test/functional/ft_38_participant_more.rb +15 -0
  74. data/test/functional/ft_39_wait_for.rb +34 -1
  75. data/test/functional/ft_3_participant_registration.rb +270 -2
  76. data/test/functional/ft_40_wait_logger.rb +61 -0
  77. data/test/functional/ft_42_storage_copy.rb +4 -0
  78. data/test/functional/{ft_40_participant_on_reply.rb → ft_43_participant_on_reply.rb} +17 -0
  79. data/test/functional/ft_44_var_participant.rb +35 -0
  80. data/test/functional/ft_45_participant_accept.rb +64 -0
  81. data/test/functional/ft_46_launch_single.rb +49 -0
  82. data/test/functional/ft_5_on_error.rb +39 -1
  83. data/test/functional/storage_helper.rb +7 -1
  84. data/test/test_helper.rb +1 -1
  85. data/test/unit/storage.rb +105 -32
  86. data/test/unit/ut_0_ruby_parser.rb +31 -1
  87. data/test/unit/ut_16_parser.rb +20 -0
  88. data/test/unit/ut_19_part_template.rb +11 -1
  89. data/test/unit/ut_20_composite_storage.rb +1 -1
  90. data/test/unit/ut_4_expmap.rb +1 -1
  91. data/test/unit/ut_6_condition.rb +2 -2
  92. metadata +112 -74
  93. data/lib/ruote/exp/raw.rb +0 -44
  94. data/lib/ruote/util/dollar.rb +0 -193
data/lib/ruote/engine.rb CHANGED
@@ -36,7 +36,7 @@ module Ruote
36
36
  # issues with stalled processes or processes stuck in errors.
37
37
  #
38
38
  # NOTE : the methods #launch and #reply are implemented in
39
- # Ruote::ReceiverMixin
39
+ # Ruote::ReceiverMixin (this Engine class has all the methods of a Receiver).
40
40
  #
41
41
  class Engine
42
42
 
@@ -45,43 +45,144 @@ module Ruote
45
45
  attr_reader :context
46
46
  attr_reader :variables
47
47
 
48
- def initialize (worker_or_storage, run=true)
48
+ # Creates an engine using either worker or storage.
49
+ #
50
+ # If a storage instance is given as the first argument, the engine will be
51
+ # able to manage processes (for example, launch and cancel workflows) but
52
+ # will not actually run any workflows.
53
+ #
54
+ # If a worker instance is given as the first argument and the second
55
+ # argument is true, engine will start the worker and will be able to both
56
+ # manage and run workflows.
57
+ #
58
+ # If the second options is set to { :join => true }, the worker wil
59
+ # be started and run in the current thread.
60
+ #
61
+ def initialize (worker_or_storage, opts=true)
49
62
 
50
63
  @context = worker_or_storage.context
51
64
  @context.engine = self
52
65
 
53
66
  @variables = EngineVariables.new(@context.storage)
54
67
 
55
- @context.worker.run_in_thread if @context.worker && run
56
- # launch the worker if there is one
68
+ if @context.worker
69
+ if opts == true
70
+ @context.worker.run_in_thread
71
+ # runs worker in its own thread
72
+ elsif opts == { :join => true }
73
+ @context.worker.run
74
+ # runs worker in current thread (and doesn't return)
75
+ #else
76
+ # worker is not run
77
+ end
78
+ #else
79
+ # no worker
80
+ end
57
81
  end
58
82
 
83
+ # Returns the storage this engine works with passed at engine
84
+ # initialization.
85
+ #
59
86
  def storage
60
87
 
61
88
  @context.storage
62
89
  end
63
90
 
91
+ # Returns the worker nested inside this engine (passed at initialization).
92
+ # Returns nil if this engine is only linked to a storage (and the worker
93
+ # is running somewhere else (hopefully)).
94
+ #
64
95
  def worker
65
96
 
66
97
  @context.worker
67
98
  end
68
99
 
100
+ # Quick note : the implementation of launch is found in the module
101
+ # Ruote::ReceiverMixin that the engine includes.
102
+ #
103
+ # Some processes have to have one and only one instance of themselves
104
+ # running, these are called 'singles' ('singleton' is too object-oriented).
105
+ #
106
+ # When called, this method will check if an instance of the pdef is
107
+ # already running (it uses the process definition name attribute), if
108
+ # yes, it will return without having launched anything. If there is no
109
+ # such process running, it will launch it (and register it).
110
+ #
111
+ # Returns the wfid (workflow instance id) of the running single.
112
+ #
113
+ def launch_single (process_definition, fields={}, variables={})
114
+
115
+ tree = @context.parser.parse(process_definition)
116
+ name = tree[1]['name'] || (tree[1].find { |k, v| v.nil? } || []).first
117
+
118
+ raise ArgumentError.new(
119
+ 'process definition is missing a name, cannot launch as single'
120
+ ) unless name
121
+
122
+ singles = @context.storage.get('variables', 'singles') || {
123
+ '_id' => 'singles', 'type' => 'variables', 'h' => {}
124
+ }
125
+ wfid, timestamp = singles['h'][name]
126
+
127
+ if wfid && (timestamp + 1.0 < Time.now.to_f || process(wfid) != nil)
128
+ return wfid
129
+ end
130
+ # process is already running
131
+
132
+ wfid = @context.wfidgen.generate
133
+
134
+ singles['h'][name] = [ wfid, Time.now.to_f ]
135
+
136
+ r = @context.storage.put(singles)
137
+
138
+ return launch_single(tree, fields, variables) unless r.nil?
139
+ #
140
+ # the put failed, back to the start...
141
+ #
142
+ # all this to prevent races between multiple engines,
143
+ # multiple launch_single calls (from different Ruby runtimes)
144
+
145
+ # ... green for launch
146
+
147
+ @context.storage.put_msg(
148
+ 'launch',
149
+ 'wfid' => wfid,
150
+ 'tree' => tree,
151
+ 'workitem' => { 'fields' => fields },
152
+ 'variables' => variables)
153
+
154
+ wfid
155
+ end
156
+
157
+ # Given a process identifier (wfid), cancels this process.
158
+ #
69
159
  def cancel_process (wfid)
70
160
 
71
161
  @context.storage.put_msg('cancel_process', 'wfid' => wfid)
72
162
  end
73
163
 
164
+ # Given a process identifier (wfid), kills this process. Killing is
165
+ # equivalent to cancelling, but when killing, :on_cancel attributes
166
+ # are not triggered.
167
+ #
74
168
  def kill_process (wfid)
75
169
 
76
170
  @context.storage.put_msg('kill_process', 'wfid' => wfid)
77
171
  end
78
172
 
173
+ # Cancels a segment of process instance. Since expressions are nodes in
174
+ # processes instances, cancelling an expression, will cancel the expression
175
+ # and all its children (the segment of process).
176
+ #
79
177
  def cancel_expression (fei)
80
178
 
81
179
  fei = fei.to_h if fei.respond_to?(:to_h)
82
180
  @context.storage.put_msg('cancel', 'fei' => fei)
83
181
  end
84
182
 
183
+ # Like #cancel_expression, but :on_cancel attributes (of the expressions)
184
+ # are not triggered.
185
+ #
85
186
  def kill_expression (fei)
86
187
 
87
188
  fei = fei.to_h if fei.respond_to?(:to_h)
@@ -142,41 +243,94 @@ module Ruote
142
243
  #
143
244
  def process (wfid)
144
245
 
145
- exps = @context.storage.get_many('expressions', /!#{wfid}$/)
146
- errs = self.errors( wfid )
147
-
148
- return nil if exps.empty? && errs.empty?
149
-
150
- ProcessStatus.new(@context, exps, errs)
246
+ list_processes([ wfid ], {}).first
151
247
  end
152
248
 
153
249
  # Returns an array of ProcessStatus instances.
154
250
  #
155
- # WARNING : this is an expensive operation.
251
+ # WARNING : this is an expensive operation, but it understands :skip
252
+ # and :limit, so pagination is our friend.
253
+ #
254
+ # Please note, if you're interested only in processes that have errors,
255
+ # Engine#errors is a more efficient means.
156
256
  #
157
- def processes
257
+ # To simply list the wfids of the currently running, Engine#process_wfids
258
+ # is way cheaper to call.
259
+ #
260
+ def processes (opts={})
158
261
 
159
- exps = @context.storage.get_many('expressions')
160
- errs = self.errors
262
+ wfids = nil
161
263
 
162
- by_wfid = {}
264
+ if opts.size > 0
163
265
 
164
- exps.each do |exp|
165
- (by_wfid[exp['fei']['wfid']] ||= [ [], [] ]).first << exp
166
- end
167
- errs.each do |err|
168
- (by_wfid[err['msg']['fei']['wfid']] ||= [ [], [] ]).last << err
266
+ wfids = @context.storage.expression_wfids(opts)
267
+
268
+ return wfids.size if opts[:count]
169
269
  end
170
270
 
171
- by_wfid.values.collect { |xs, rs| ProcessStatus.new(@context, xs, rs) }
271
+ list_processes(wfids, opts)
172
272
  end
173
273
 
174
274
  # Returns an array of current errors (hashes)
175
275
  #
176
- def errors( wfid = nil )
177
- wfid.nil? ?
178
- @context.storage.get_many('errors') :
179
- @context.storage.get_many('errors', /!#{wfid}$/)
276
+ # Can be called in two ways :
277
+ #
278
+ # engine.errors(wfid)
279
+ #
280
+ # and
281
+ #
282
+ # engine.errors(:skip => 100, :limit => 100)
283
+ #
284
+ def errors (wfid=nil)
285
+
286
+ wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
287
+
288
+ errs = wfid.nil? ?
289
+ @context.storage.get_many('errors', nil, options) :
290
+ @context.storage.get_many('errors', wfid)
291
+
292
+ return errs if options[:count]
293
+
294
+ errs.collect { |err| ProcessError.new(err) }
295
+ end
296
+
297
+ # Returns an array of schedules. Those schedules are open structs
298
+ # with various properties, like target, owner, at, put_at, ...
299
+ #
300
+ # Introduced mostly for ruote-kit.
301
+ #
302
+ # Can be called in two ways :
303
+ #
304
+ # engine.schedules(wfid)
305
+ #
306
+ # and
307
+ #
308
+ # engine.schedules(:skip => 100, :limit => 100)
309
+ #
310
+ def schedules (wfid=nil)
311
+
312
+ wfid, options = wfid.is_a?(Hash) ? [ nil, wfid ] : [ wfid, {} ]
313
+
314
+ scheds = wfid.nil? ?
315
+ @context.storage.get_many('schedules', nil, options) :
316
+ @context.storage.get_many('schedules', /!#{wfid}-\d+$/)
317
+
318
+ return scheds if options[:count]
319
+
320
+ scheds.collect { |sched| Ruote.schedule_to_h(sched) }
321
+ end
322
+
323
+ # Returns a [sorted] list of wfids of the process instances currently
324
+ # running in the engine.
325
+ #
326
+ # This operation is substantially less costly than Engine#processes (though
327
+ # the 'how substantially' depends on the storage chosen).
328
+ #
329
+ def process_wfids
330
+
331
+ @context.storage.ids('expressions').collect { |sfei|
332
+ sfei.split('!').last
333
+ }.uniq.sort
180
334
  end
181
335
 
182
336
  # Shuts down the engine, mostly passes the shutdown message to the other
@@ -187,9 +341,13 @@ module Ruote
187
341
  @context.shutdown
188
342
  end
189
343
 
190
- # This method expects there is a logger with a wait_for method in the
344
+ # This method expects there to be a logger with a wait_for method in the
191
345
  # context, else it will raise an exception.
192
346
  #
347
+ # *WARNING* : wait_for() is meant for environments where there is a unique
348
+ # worker and that worker is nested in this engine. In a multiple worker
349
+ # environment wait_for doesn't see events handled by 'other' workers.
350
+ #
193
351
  # This method is only useful for test/quickstart/examples environments.
194
352
  #
195
353
  # engine.wait_for(:alpha)
@@ -224,6 +382,14 @@ module Ruote
224
382
  logger.wait_for(items)
225
383
  end
226
384
 
385
+ # Joins the worker thread. If this engine has no nested worker, calling
386
+ # this method will simply return immediately.
387
+ #
388
+ def join
389
+
390
+ worker.join if worker
391
+ end
392
+
227
393
  # Loads and parses the process definition at the given path.
228
394
  #
229
395
  def load_definition (path)
@@ -293,11 +459,27 @@ module Ruote
293
459
  # end
294
460
  # end
295
461
  #
296
- # engine.register_participant 'moon', MyStatelessParticipant, 'name' => 'saturn5'
462
+ # engine.register_participant(
463
+ # 'moon', MyStatelessParticipant, 'name' => 'saturn5')
297
464
  #
298
465
  # Remember that the options (the hash that follows the class name), must be
299
466
  # serialisable via JSON.
300
467
  #
468
+ #
469
+ # == require_path and load_path
470
+ #
471
+ # It's OK to register a participant by passing its full classname as a
472
+ # String.
473
+ #
474
+ # engine.register_participant(
475
+ # 'auditor', 'AuditParticipant', 'require_path' => 'part/audit.rb')
476
+ # engine.register_participant(
477
+ # 'auto_decision', 'DecParticipant', 'load_path' => 'part/dec.rb')
478
+ #
479
+ # Note the option load_path / require_path that point to the ruby file
480
+ # containing the participant implementation. 'require' will load and eval
481
+ # the ruby code only once, 'load' each time.
482
+ #
301
483
  def register_participant (regex, participant=nil, opts={}, &block)
302
484
 
303
485
  pa = @context.plist.register(regex, participant, opts, block)
@@ -310,6 +492,30 @@ module Ruote
310
492
  pa
311
493
  end
312
494
 
495
+ # A shorter version of #register_participant
496
+ #
497
+ # engine.register 'alice', MailParticipant, :target => 'alice@example.com'
498
+ #
499
+ # or a block registering mechanism.
500
+ #
501
+ # engine.register do
502
+ # alpha 'Participants::Alpha', 'flavour' => 'vanilla'
503
+ # participant 'bravo', 'Participants::Bravo', :flavour => 'peach'
504
+ # catchall ParticipantCharlie, 'flavour' => 'coconut'
505
+ # end
506
+ #
507
+ # Originally implemented in ruote-kit by Torsten Schoenebaum.
508
+ #
509
+ def register (*args, &block)
510
+
511
+ if args.size > 0
512
+ register_participant(*args, &block)
513
+ else
514
+ proxy = ParticipantRegistrationProxy.new(self)
515
+ block.arity < 1 ? proxy.instance_eval(&block) : block.call(proxy)
516
+ end
517
+ end
518
+
313
519
  # Removes/unregisters a participant from the engine.
314
520
  #
315
521
  def unregister_participant (name_or_participant)
@@ -323,6 +529,55 @@ module Ruote
323
529
  'regex' => re.to_s)
324
530
  end
325
531
 
532
+ alias :unregister :unregister_participant
533
+
534
+ # Returns a list of Ruote::ParticipantEntry instances.
535
+ #
536
+ # engine.register_participant :alpha, MyParticipant, 'message' => 'hello'
537
+ #
538
+ # # interrogate participant list
539
+ # #
540
+ # list = engine.participant_list
541
+ # participant = list.first
542
+ # p participant.regex
543
+ # # => "^alpha$"
544
+ # p participant.classname
545
+ # # => "MyParticipant"
546
+ # p participant.options
547
+ # # => {"message"=>"hello"}
548
+ #
549
+ # # update participant list
550
+ # #
551
+ # participant.regex = '^alfred$'
552
+ # engine.participant_list = list
553
+ #
554
+ def participant_list
555
+
556
+ @context.plist.list
557
+ end
558
+
559
+ # Accepts a list of Ruote::ParticipantEntry instances.
560
+ #
561
+ # See Engine#participant_list
562
+ #
563
+ def participant_list= (pl)
564
+
565
+ @context.plist.list = pl
566
+ end
567
+
568
+ # A convenience method for
569
+ #
570
+ # sp = Ruote::StorageParticipant.new(engine)
571
+ #
572
+ # simply do
573
+ #
574
+ # sp = engine.storage_participant
575
+ #
576
+ def storage_participant
577
+
578
+ @storage_participant ||= Ruote::StorageParticipant.new(self)
579
+ end
580
+
326
581
  # Adds a service locally (will not get propagated to other workers).
327
582
  #
328
583
  # tracer = Tracer.new
@@ -373,6 +628,64 @@ module Ruote
373
628
 
374
629
  Ruote::Workitem.new(fexp.h.applied_workitem)
375
630
  end
631
+
632
+ # A debug helper :
633
+ #
634
+ # engine.noisy = true
635
+ #
636
+ # will let the engine (in fact the worker) pour all the details of the
637
+ # executing process instances to STDOUT.
638
+ #
639
+ def noisy= (b)
640
+
641
+ @context.logger.noisy = b
642
+ end
643
+
644
+ protected
645
+
646
+ # Used by #process and #processes
647
+ #
648
+ def list_processes (wfids, opts)
649
+
650
+ swfids = wfids ? wfids.collect { |wfid| /!#{wfid}-\d+$/ } : nil
651
+
652
+ exps = @context.storage.get_many('expressions', wfids)
653
+ swis = @context.storage.get_many('workitems', wfids)
654
+ errs = @context.storage.get_many('errors', wfids)
655
+ schs = @context.storage.get_many('schedules', swfids)
656
+
657
+ errs = errs.collect { |err| ProcessError.new(err) }
658
+ schs = schs.collect { |sch| Ruote.schedule_to_h(sch) }
659
+
660
+ by_wfid = {}
661
+
662
+ exps.each do |exp|
663
+ (by_wfid[exp['fei']['wfid']] ||= [ [], [], [], [] ])[0] << exp
664
+ end
665
+ swis.each do |swi|
666
+ (by_wfid[swi['fei']['wfid']] ||= [ [], [], [], [] ])[1] << swi
667
+ end
668
+ errs.each do |err|
669
+ (by_wfid[err.wfid] ||= [ [], [], [], [] ])[2] << err
670
+ end
671
+ schs.each do |sch|
672
+ (by_wfid[sch['wfid']] ||= [ [], [], [], [] ])[3] << sch
673
+ end
674
+
675
+ wfids = if wfids
676
+ wfids
677
+ else
678
+ wfids = by_wfid.keys.sort
679
+ wfids = wfids.reverse if opts[:descending]
680
+ wfids
681
+ end
682
+
683
+ wfids.inject([]) { |a, wfid|
684
+ info = by_wfid[wfid]
685
+ a << ProcessStatus.new(@context, *info) if info
686
+ a
687
+ }
688
+ end
376
689
  end
377
690
 
378
691
  #
@@ -398,5 +711,59 @@ module Ruote
398
711
  @storage.put_engine_variable(k, v)
399
712
  end
400
713
  end
714
+
715
+ #
716
+ # Engine#register uses this proxy when it's passed a block.
717
+ #
718
+ # Originally written by Torsten Schoenebaum for ruote-kit.
719
+ #
720
+ class ParticipantRegistrationProxy
721
+
722
+ def initialize (engine)
723
+
724
+ @engine = engine
725
+ end
726
+
727
+ def participant (name, klass, options={})
728
+
729
+ @engine.register_participant(name, klass, options)
730
+ end
731
+
732
+ def catchall (*args)
733
+
734
+ klass = args.empty? ? Ruote::StorageParticipant : args.first
735
+ options = args[1] || {}
736
+
737
+ participant('.+', klass, options)
738
+ end
739
+
740
+ # Maybe a bit audacious...
741
+ #
742
+ def method_missing (method_name, *args)
743
+
744
+ participant(method_name, *args)
745
+ end
746
+ end
747
+
748
+ # Refines a schedule as found in the ruote storage into something a bit
749
+ # easier to present.
750
+ #
751
+ def self.schedule_to_h (sched)
752
+
753
+ h = sched.dup
754
+
755
+ h.delete('_rev')
756
+ h.delete('type')
757
+ msg = h.delete('msg')
758
+ owner = h.delete('owner')
759
+
760
+ h['wfid'] = owner['wfid']
761
+ h['action'] = msg['action']
762
+ h['type'] = msg['flavour']
763
+ h['owner'] = Ruote::FlowExpressionId.new(owner)
764
+ h['target'] = Ruote::FlowExpressionId.new(msg['fei'])
765
+
766
+ h
767
+ end
401
768
  end
402
769
 
@@ -43,6 +43,8 @@ module Ruote::Exp
43
43
  command, step = lookup_attribute_command(workitem) unless command
44
44
  command = 'break' if command == 'over' || command == 'stop'
45
45
 
46
+ step = 1 if step == ''
47
+
46
48
  return nil if command == nil
47
49
 
48
50
  if command == 'back'
@@ -99,16 +99,24 @@ module Ruote::Exp
99
99
  #
100
100
  # === :merge_type
101
101
  #
102
+ # ==== :override
103
+ #
102
104
  # By default, the merge type is set to 'override', which means that the
103
105
  # 'winning' workitem's payload supplants all other workitems' payloads.
104
106
  #
107
+ # ==== :mix
108
+ #
105
109
  # Setting :merge_type to :mix, will actually attempt to merge field by field,
106
110
  # making sure that the field value of the winner(s) are used.
107
111
  #
112
+ # ==== :isolate
113
+ #
108
114
  # :isolate will rearrange the resulting workitem payload so that there is
109
115
  # a new field for each branch. The name of each field is the index of the
110
116
  # branch from '0' to ...
111
117
  #
118
+ # ==== :stack
119
+ #
112
120
  # :stack will stack the workitems coming back from the concurrence branches
113
121
  # in an array whose order is determined by the :merge attributes. The array
114
122
  # is placed in the 'stack' field of the resulting workitem.
@@ -32,6 +32,9 @@ module Ruote::Exp
32
32
  #
33
33
  # This expression is a cross between 'concurrence' and 'iterator'.
34
34
  #
35
+ # Please look at the documentation of 'iterator' to learn more about the
36
+ # common options between 'iterator' and 'concurrent-iterator'.
37
+ #
35
38
  # pdef = Ruote.process_definition :name => 'test' do
36
39
  # concurrent_iterator :on_val => 'alice, bob, charly', :to_var => 'v' do
37
40
  # participant '${v:v}'
@@ -66,7 +66,7 @@ module Ruote::Exp
66
66
  # publisher
67
67
  # end
68
68
  #
69
- # === break
69
+ # === stop, over & break
70
70
  #
71
71
  # Exits the cursor.
72
72
  #
@@ -74,10 +74,12 @@ module Ruote::Exp
74
74
  # author
75
75
  # reviewer
76
76
  # rewind :if => '${f:review} == fix'
77
- # _break :if => '${f:review} == abort'
77
+ # stop :if => '${f:review} == abort'
78
78
  # publisher
79
79
  # end
80
80
  #
81
+ # '_break' or 'over' can be used instead of 'stop'.
82
+ #
81
83
  # === skip & back
82
84
  #
83
85
  # Those two commands jump forth and back respectively. By default, they
@@ -159,6 +161,44 @@ module Ruote::Exp
159
161
  # cursor, but it will break the main 'cursor' (and thus break the whole
160
162
  # review process).
161
163
  #
164
+ # == cursor command in the workitem
165
+ #
166
+ # The command expressions are merely setting the workitem field '__command__'
167
+ # with an array value [ {command}, {arg} ].
168
+ #
169
+ # For example,
170
+ #
171
+ # jump :to => 'author'
172
+ # # is equivalent to
173
+ # set 'field:__command__' => 'author'
174
+ #
175
+ # It is entirely OK to have a participant implementation that sets __command__
176
+ # by itself.
177
+ #
178
+ # class Reviewer
179
+ # include Ruote::LocalParticipant
180
+ #
181
+ # def consume (workitem)
182
+ # # somehow review the book
183
+ # if review == 'bad'
184
+ # #workitem.fields['__command__'] = [ 'rewind' ] # old style
185
+ # workitem.command = 'rewind' # new style
186
+ # else
187
+ # # let it go
188
+ # end
189
+ # reply_to_engine(workitem)
190
+ # end
191
+ #
192
+ # def cancel (fei, flavour)
193
+ # # cancel if review is still going on...
194
+ # end
195
+ # end
196
+ #
197
+ # This example uses the Ruote::Workitem#command= method which can be fed
198
+ # strings like 'rewind', 'skip 2', 'jump to author' or the equivalent arrays
199
+ # [ 'rewind' ], [ 'skip', 2 ], [ 'jump', 'author' ].
200
+ #
201
+ #
162
202
  # == :break_if / :rewind_if
163
203
  #
164
204
  # As an attribute of the cursor/repeat expression, you can set a :break_if.
@@ -208,6 +248,8 @@ module Ruote::Exp
208
248
 
209
249
  protected
210
250
 
251
+ # Determines which child expression of the cursor is to be applied next.
252
+ #
211
253
  def move_on (workitem=h.applied_workitem)
212
254
 
213
255
  position = workitem['fei'] == h.fei ?
@@ -234,6 +276,8 @@ module Ruote::Exp
234
276
  end
235
277
  end
236
278
 
279
+ # Will return true if this instance is about a 'loop' or a 'repeat'.
280
+ #
237
281
  def is_loop?
238
282
 
239
283
  name == 'loop' || name == 'repeat'
@@ -254,8 +298,8 @@ module Ruote::Exp
254
298
  ref = c[1]['ref']
255
299
  tag = c[1]['tag']
256
300
 
257
- ref = Ruote.dosub(ref, self, workitem) if ref
258
- tag = Ruote.dosub(tag, self, workitem) if tag
301
+ ref = @context.dollar_sub.s(ref, self, workitem) if ref
302
+ tag = @context.dollar_sub.s(tag, self, workitem) if tag
259
303
 
260
304
  next if exp_name != arg && ref != arg && tag != arg
261
305