ruote 2.1.10 → 2.1.11

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