spot_flow 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec2c62e8ee6f2863a951627cfe2733f5d015feb981cd036bbabff22c7d681ea0
4
+ data.tar.gz: 416207e69a02f5c8ba3308369c46bdcc783a162ed9eb45ef1f8788ec6dbc0dc7
5
+ SHA512:
6
+ metadata.gz: 0a0526b9ef739fdb028d6864f4144a9e7e58da25eca2553acfe82933175c82d3d85818e12f33884a5e9efda650b78698aefb093dbd4a70347952a178dc4cd436
7
+ data.tar.gz: 16c1a43d577b831a774c57e748e57c78c6750eec256aef6bf9cbf5991996aa33fdacf33ecbc37e2e327c21573b525dd2bafffc6dc58216e938fa3729d848f70b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Connected Bits, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # Spot Flow
2
+
3
+ Spot Flow is a workflow engine for Rails applications based on the [bpmn](https://www.bpmn.org) standard. It executes business processes defined in a [modeler](https://camunda.com/download/modeler/). It uses [Spot Feel](https://github.com/connectedbits/spot-feel) to evaluate expressions and business rules in the BPMN model. It can be used with [Spot Form](https://github.com/connectedbits/spot-form) to render dynamic forms for user tasks.
4
+
5
+ ## Usage
6
+
7
+ The engine executes business processes like [this one](/test/fixtures/files/hello_world.bpmn).
8
+
9
+ ![Example](test/fixtures/files/hello_world.png)
10
+
11
+ Processes are made of a series of tasks. Tasks can be manual (require `signal` to be called) or automated (can be `run` by the engine). The engine supports the following task types:
12
+
13
+ - Task and UserTask (manual): requires a signal to continue.
14
+ - ServiceTask (automated): instantiates a type defined in the task definition and invokes `call` on it.
15
+ - BusinessRuleTask (automated): evaluates the decision_id (expects dmn source to be included in the context).
16
+ - ScriptTask (automated): evaluates the FEEL expression in the script property.
17
+
18
+ To start the process, initialize SpotFlow with the BPMN and DMN source files, then call `start`.
19
+
20
+ ```ruby
21
+ sources = [
22
+ File.read("hello_world.bpmn"),
23
+ File.read("choose_greeting.dmn")
24
+ ]
25
+ execution = SpotFlow.new(sources).start
26
+ ```
27
+
28
+ It's often useful to print the process state to the console.
29
+
30
+ ```ruby
31
+ execution.print
32
+ ```
33
+
34
+ ```ruby
35
+ HelloWorld started * Flow_080794y
36
+
37
+ 0 StartEvent Start: completed * out: Flow_080794y
38
+ 1 UserTask IntroduceYourself: waiting * in: Flow_080794y
39
+ 2 BoundaryEvent EggTimer: waiting
40
+ ```
41
+
42
+ The HelloWorld process began at the Start event and is _waiting_ at the IntroduceYourself user task. This is an important concept in the SpotFlow engine. It's designed to be used in a Rails application where a process might be waiting for a user to complete a form, or a background job to complete. It's common to save the state the process until a task is complete. Calling `serialize` on a process will return the execution state so it can be continued later.
43
+
44
+ ```ruby
45
+ # Returns a hash of the process state.
46
+ execution_state = execution.serialize
47
+
48
+ # Now we can save the execution state in a database until a user submits a form (UserTask)
49
+ # or a background job completes (ServiceTask)
50
+
51
+ # Restores the process from the execution state.
52
+ execution = SpotFlow.restore(sources, execution_state:)
53
+
54
+ # Now we can continue the process by `signaling` the waiting task.
55
+ step = execution.step_by_element_id("IntroduceYourself")
56
+ step.signal(name: "Eric", language: "it", formal: false, cookie: true)
57
+ ```
58
+
59
+ After the IntroduceYourself task is _signaled_, the execution continues.
60
+
61
+ ```ruby
62
+ HelloWorld started * Flow_0gi9kt6, Flow_0pb7kh6
63
+
64
+ {
65
+ "name": "Eric",
66
+ "language": "it",
67
+ "formal": false,
68
+ "cookie": true
69
+ }
70
+
71
+ 0 StartEvent Start: completed * out: Flow_080794y
72
+ 1 UserTask IntroduceYourself: completed { "name": "Eric", "language": "it", "formal": false, "cookie": true } * in: Flow_080794y * out: Flow_0t9jhga
73
+ 2 BoundaryEvent EggTimer: terminated
74
+ 3 ParallelGateway Split: completed * in: Flow_0t9jhga * out: Flow_0gi9kt6, Flow_0q1vtg3
75
+ 4 BusinessRuleTask ChooseGreeting: waiting * in: Flow_0gi9kt6
76
+ 5 ExclusiveGateway WantsCookie: completed * in: Flow_0q1vtg3 * out: Flow_0pb7kh6
77
+ 6 ServiceTask ChooseFortune: waiting * in: Flow_0pb7kh6
78
+ ```
79
+
80
+ When execution reaches the Split inclusive gateway, it forks into two paths. The first path is _waiting_ at the ChooseGreeting business rule task. The second reaches the WantsCookie exclusive gateway and _evaluates_ the sequence flow conditions before continuing to the ChooseFortune service task. Automated tasks can be `run` individually by the engine or `run_automated_tasks` to run all _waiting_ tasks.
81
+
82
+ ```ruby
83
+ execution.run_automated_tasks
84
+ ```
85
+
86
+ Now, both paths are joined into one and execution is _waiting_ at the script task. Notice, the results of previous tasks are merged into the process variables.
87
+
88
+ ```ruby
89
+ HelloWorld started * Flow_0ws7a4m
90
+
91
+ {
92
+ "name": "Eric",
93
+ "language": "it",
94
+ "formal": false,
95
+ "cookie": true,
96
+ "choose_greeting": {
97
+ "greeting": "Ciao"
98
+ },
99
+ "choose_fortune": "A foolish man listens to his heart. A wise man listens to cookies."
100
+ }
101
+
102
+ 0 StartEvent Start: completed * out: Flow_080794y
103
+ 1 UserTask IntroduceYourself: completed { "name": "Eric", "language": "it", "formal": false, "cookie": true } * in: Flow_080794y * out: Flow_0t9jhga
104
+ 2 BoundaryEvent EggTimer: terminated
105
+ 3 ParallelGateway Split: completed * in: Flow_0t9jhga * out: Flow_0gi9kt6, Flow_0q1vtg3
106
+ 4 BusinessRuleTask ChooseGreeting: completed { "choose_greeting": { "greeting": "Ciao" } } * in: Flow_0gi9kt6 * out: Flow_1652shv
107
+ 5 ExclusiveGateway WantsCookie: completed * in: Flow_0q1vtg3 * out: Flow_0pb7kh6
108
+ 6 ServiceTask ChooseFortune: completed { "choose_fortune": "A foolish man listens to his heart. A wise man listens to cookies." } * in: Flow_0pb7kh6 * out: Flow_1iuc1xe
109
+ 7 ParallelGateway Join: completed * in: Flow_1652shv, Flow_1iuc1xe * out: Flow_0ws7a4m
110
+ 8 ScriptTask GenerateMessage: waiting * in: Flow_0ws7a4m
111
+ ```
112
+
113
+ This time we'll `run` the script task manually.
114
+
115
+ ```ruby
116
+ step = execution.step_by_element_id("GenerateMessage")
117
+ step.run
118
+ ```
119
+
120
+ Now the process is complete.
121
+
122
+ ```ruby
123
+ HelloWorld completed *
124
+
125
+ {
126
+ "name": "Eric",
127
+ "language": "it",
128
+ "formal": false,
129
+ "cookie": true,
130
+ "choose_greeting": {
131
+ "greeting": "Ciao"
132
+ },
133
+ "choose_fortune": "A foolish man listens to his heart. A wise man listens to cookies.",
134
+ "message": "👋 Ciao Eric 🥠 A foolish man listens to his heart. A wise man listens to cookies."
135
+ }
136
+
137
+ 0 StartEvent Start: completed * out: Flow_080794y
138
+ 1 UserTask IntroduceYourself: completed { "name": "Eric", "language": "it", "formal": false, "cookie": true } * in: Flow_080794y * out: Flow_0t9jhga
139
+ 2 BoundaryEvent EggTimer: terminated
140
+ 3 ParallelGateway Split: completed * in: Flow_0t9jhga * out: Flow_0gi9kt6, Flow_0q1vtg3
141
+ 4 BusinessRuleTask ChooseGreeting: completed { "choose_greeting": { "greeting": "Ciao" } } * in: Flow_0gi9kt6 * out: Flow_1652shv
142
+ 5 ExclusiveGateway WantsCookie: completed * in: Flow_0q1vtg3 * out: Flow_0pb7kh6
143
+ 6 ServiceTask ChooseFortune: completed { "choose_fortune": "A foolish man listens to his heart. A wise man listens to cookies." } * in: Flow_0pb7kh6 * out: Flow_1iuc1xe
144
+ 7 ParallelGateway Join: completed * in: Flow_1652shv, Flow_1iuc1xe * out: Flow_0ws7a4m
145
+ 8 ScriptTask GenerateMessage: completed { "message": "👋 A foolish man listens to his heart. A wise man listens to cookies." } * in: Flow_0ws7a4m * out: Flow_0gkfhvr
146
+ 9 EndEvent End: completed * in: Flow_0gkfhvr
147
+ ```
148
+
149
+ ## Documentation
150
+
151
+ - [Processes](/docs/processes.md)
152
+ - [Tasks](/docs/tasks.md)
153
+ - [Events](/docs/events.md)
154
+ - [Event Definitions](/docs/event_definitions.md)
155
+ - [Gateways](/docs/gateways.md)
156
+ - [Expressions](/docs/expressions.md)
157
+ - [Data Flow](/docs/data_flow.md)
158
+ - [Execution](/docs/execution.md)
159
+
160
+ ## Installation
161
+
162
+ Execute:
163
+
164
+ ```bash
165
+ $ bundle add spot_flow
166
+ ```
167
+
168
+ Or install it directly:
169
+
170
+ ```bash
171
+ $ gem install spot_flow
172
+ ```
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ $ git clone ...
178
+ $ bin/setup
179
+ $ bin/rake
180
+ $ bin/guard
181
+ ```
182
+
183
+ ## License
184
+
185
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
186
+
187
+ Developed by [Connected Bits](http://www.connectedbits.com)
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/setup"
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFlow
4
+ module Bpmn
5
+ class Definitions
6
+ include ActiveModel::Model
7
+
8
+ attr_accessor :id, :name, :target_namespace, :exporter, :exporter_version, :execution_platform, :execution_platform_version
9
+ attr_reader :messages, :signals, :errors, :processes
10
+
11
+ def self.from_xml(xml)
12
+ XmlHasher.configure do |config|
13
+ config.snakecase = true
14
+ config.ignore_namespaces = true
15
+ config.string_keys = false
16
+ end
17
+ hash = XmlHasher.parse(xml)
18
+ Definitions.new(hash[:definitions].except(:bpmn_diagram)).tap do |definitions|
19
+ definitions.processes.each do |process|
20
+ process.wire_references(definitions)
21
+ end
22
+ end
23
+ end
24
+
25
+ def initialize(attributes={})
26
+ super(attributes.except(:message, :signal, :error, :process))
27
+
28
+ @messages = Array.wrap(attributes[:message]).map { |atts| Message.new(atts) }
29
+ @signals = Array.wrap(attributes[:signal]).map { |atts| Signal.new(atts) }
30
+ @errors = Array.wrap(attributes[:error]).map { |atts| Error.new(atts) }
31
+ @processes = Array.wrap(attributes[:process]).map { |atts| Process.new(atts) }
32
+ end
33
+
34
+ def message_by_id(id)
35
+ messages.find { |message| message.id == id }
36
+ end
37
+
38
+ def signal_by_id(id)
39
+ signals.find { |signal| signal.id == id }
40
+ end
41
+
42
+ def error_by_id(id)
43
+ errors.find { |error| error.id == id }
44
+ end
45
+
46
+ def process_by_id(id)
47
+ processes.find { |process| process.id == id }
48
+ end
49
+
50
+ def inspect
51
+ "#<Bpmn::Definitions @id=#{id.inspect} @name=#{name.inspect} @messages=#{messages.inspect} @signals=#{signals.inspect} @errors=#{errors.inspect} @processes=#{processes.inspect}>"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFlow
4
+ module Bpmn
5
+ class Element
6
+ include ActiveModel::Model
7
+
8
+ attr_accessor :id, :name, :extension_elements
9
+
10
+ def initialize(attributes = {})
11
+ super(attributes.slice(:id, :name))
12
+
13
+ @extension_elements = ExtensionElements.new(attributes[:extension_elements]) if attributes[:extension_elements].present?
14
+ end
15
+
16
+ def inspect
17
+ "#<#{self.class.name.gsub(/SpotFlow::/, "")} @id=#{id.inspect} @name=#{name.inspect}>"
18
+ end
19
+ end
20
+
21
+ class Message < Element
22
+ end
23
+
24
+ class Signal < Element
25
+ end
26
+
27
+ class Error < Element
28
+ end
29
+
30
+ class Collaboration < Element
31
+ end
32
+
33
+ class LaneSet < Element
34
+ end
35
+
36
+ class Participant < Element
37
+ attr_accessor :process_ref, :process
38
+
39
+ def initialize(attributes = {})
40
+ super(attributes.except(:process_ref))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFlow
4
+ module Bpmn
5
+ class Event < Step
6
+ attr_accessor :event_definitions
7
+
8
+ def initialize(attributes = {})
9
+ super(attributes.except(:message_event_definition, :signal_event_definition, :error_event_definition, :terminate_event_definition, :timer_event_definition))
10
+
11
+ @event_definitions = []
12
+
13
+ Array.wrap(attributes[:message_event_definition]).each do |med|
14
+ @event_definitions.push MessageEventDefinition.new(med)
15
+ end if attributes[:message_event_definition].present?
16
+
17
+ Array.wrap(attributes[:signal_event_definition]).each do |sed|
18
+ @event_definitions.push SignalEventDefinition.new(sed)
19
+ end if attributes[:signal_event_definition].present?
20
+
21
+ Array.wrap(attributes[:error_event_definition]).each do |eed|
22
+ @event_definitions.push ErrorEventDefinition.new(eed)
23
+ end if attributes[:error_event_definition].present?
24
+
25
+ Array.wrap(attributes[:terminate_event_definition]).each do |ted|
26
+ @event_definitions.push TerminateEventDefinition.new(ted)
27
+ end if attributes[:terminate_event_definition].present?
28
+
29
+ Array.wrap(attributes[:timer_event_definition]).each do |ted|
30
+ @event_definitions.push TimerEventDefinition.new(ted)
31
+ end if attributes[:timer_event_definition].present?
32
+ end
33
+
34
+ def event_definition_ids
35
+ event_definitions.map(&:id)
36
+ end
37
+
38
+ def is_catching?
39
+ false
40
+ end
41
+
42
+ def is_throwing?
43
+ false
44
+ end
45
+
46
+ def is_none?
47
+ event_definitions.empty?
48
+ end
49
+
50
+ def is_conditional?
51
+ conditional_event_definition.present?
52
+ end
53
+
54
+ def is_escalation?
55
+ escalation_event_definition.present?
56
+ end
57
+
58
+ def is_error?
59
+ error_event_definition.present?
60
+ end
61
+
62
+ def is_message?
63
+ !message_event_definitions.empty?
64
+ end
65
+
66
+ def is_signal?
67
+ !signal_event_definitions.empty?
68
+ end
69
+
70
+ def is_terminate?
71
+ terminate_event_definition.present?
72
+ end
73
+
74
+ def is_timer?
75
+ timer_event_definition.present?
76
+ end
77
+
78
+ def conditional_event_definition
79
+ event_definitions.find { |ed| ed.is_a?(ConditionalEventDefinition) }
80
+ end
81
+
82
+ def escalation_event_definition
83
+ event_definitions.find { |ed| ed.is_a?(EscalationEventDefinition) }
84
+ end
85
+
86
+ def error_event_definitions
87
+ event_definitions.select { |ed| ed.is_a?(ErrorEventDefinition) }
88
+ end
89
+
90
+ def error_event_definition
91
+ event_definitions.find { |ed| ed.is_a?(ErrorEventDefinition) }
92
+ end
93
+
94
+ def message_event_definitions
95
+ event_definitions.select { |ed| ed.is_a?(MessageEventDefinition) }
96
+ end
97
+
98
+ def signal_event_definitions
99
+ event_definitions.select { |ed| ed.is_a?(SignalEventDefinition) }
100
+ end
101
+
102
+ def terminate_event_definition
103
+ event_definitions.find { |ed| ed.is_a?(TerminateEventDefinition) }
104
+ end
105
+
106
+ def timer_event_definition
107
+ event_definitions.find { |ed| ed.is_a?(TimerEventDefinition) }
108
+ end
109
+
110
+ def execute(execution)
111
+ event_definitions.each { |event_definition| event_definition.execute(execution) }
112
+ end
113
+ end
114
+
115
+ class StartEvent < Event
116
+
117
+ def is_catching?
118
+ true
119
+ end
120
+
121
+ def execute(execution)
122
+ super
123
+ leave(execution)
124
+ end
125
+ end
126
+
127
+ class IntermediateThrowEvent < Event
128
+
129
+ def is_throwing?
130
+ true
131
+ end
132
+
133
+ def execute(execution)
134
+ super
135
+ leave(execution)
136
+ end
137
+ end
138
+
139
+ class IntermediateCatchEvent < Event
140
+
141
+ def is_catching?
142
+ true
143
+ end
144
+
145
+ def execute(execution)
146
+ super
147
+ execution.wait
148
+ end
149
+
150
+ def signal(execution)
151
+ leave(execution)
152
+ end
153
+ end
154
+
155
+ class BoundaryEvent < Event
156
+ attr_accessor :attached_to_ref, :attached_to, :cancel_activity
157
+
158
+ def initialize(attributes = {})
159
+ super(attributes.except(:attached_to_ref, :cancel_activity))
160
+
161
+ @attached_to_ref = attributes[:attached_to_ref]
162
+ @cancel_activity = true
163
+ if attributes[:cancel_activity].present?
164
+ @cancel_activity = attributes[:cancel_activity] == "false" ? false : true
165
+ end
166
+ end
167
+
168
+ def is_catching?
169
+ true
170
+ end
171
+
172
+ def execute(execution)
173
+ super
174
+ execution.wait
175
+ end
176
+
177
+ def signal(execution)
178
+ execution.attached_to.terminate if cancel_activity
179
+ leave(execution)
180
+ end
181
+ end
182
+
183
+ class EndEvent < Event
184
+
185
+ def is_throwing?
186
+ true
187
+ end
188
+
189
+ def execute(execution)
190
+ super
191
+ execution.end(true)
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFlow
4
+ module Bpmn
5
+ class EventDefinition < Element
6
+ def execute(execution)
7
+ end
8
+ end
9
+
10
+ class ConditionalEventDefinition < EventDefinition
11
+ attr_accessor :variable_name, :variable_events, :condition
12
+
13
+ def initialize(attributes = {})
14
+ super(attributes.except(:variable_name, :variable_events, :condition))
15
+
16
+ @variable_name = moddle[:variable_name] # "var1"
17
+ @variable_events = moddle[:variable_events] # "create, update"
18
+ @condition = moddle[:condition] # var1 = 1
19
+ end
20
+ end
21
+
22
+ class EscalationEventDefinition < EventDefinition
23
+ end
24
+
25
+ class ErrorEventDefinition < EventDefinition
26
+ attr_accessor :error_ref, :error
27
+ attr_accessor :error_code_variable, :error_message_variable
28
+
29
+ def initialize(attributes = {})
30
+ super(attributes.except(:error_ref, :error_code_variable, :error_message_variable))
31
+
32
+ @error_ref = attributes[:error_ref]
33
+ @error_code_variable = attributes[:error_code_variable]
34
+ @error_message_variable = attributes[:error_message_variable]
35
+ end
36
+
37
+ def execute(execution)
38
+ if execution.step.is_throwing?
39
+ execution.throw_error(error_name)
40
+ else
41
+ execution.error_names.push error_name
42
+ end
43
+ end
44
+
45
+ def error_id
46
+ error&.id
47
+ end
48
+
49
+ def error_name
50
+ error&.name
51
+ end
52
+ end
53
+
54
+ class MessageEventDefinition < EventDefinition
55
+ attr_accessor :message_ref, :message
56
+
57
+ def initialize(attributes = {})
58
+ super(attributes.except(:message_ref))
59
+
60
+ @message_ref = attributes[:message_ref]
61
+ end
62
+
63
+ def execute(execution)
64
+ if execution.step.is_throwing?
65
+ execution.throw_message(message_name)
66
+ else
67
+ execution.message_names.push message_name
68
+ end
69
+ end
70
+
71
+ def message_id
72
+ message&.id
73
+ end
74
+
75
+ def message_name
76
+ message&.name
77
+ end
78
+ end
79
+
80
+ class SignalEventDefinition < EventDefinition
81
+ attr_accessor :signal_ref, :signal
82
+
83
+ def initialize(attributes = {})
84
+ super(attributes.except(:signal_ref))
85
+
86
+ @signal_ref = moddle[:signal_ref]
87
+ end
88
+
89
+ def signal_id
90
+ signal&.id
91
+ end
92
+
93
+ def signal_name
94
+ signal&.name
95
+ end
96
+ end
97
+
98
+ class TerminateEventDefinition < EventDefinition
99
+
100
+ def execute(execution)
101
+ execution.parent&.terminate
102
+ end
103
+ end
104
+
105
+ class TimerEventDefinition < EventDefinition
106
+ attr_accessor :time_date, :time_duration_type, :time_duration, :time_cycle
107
+
108
+ def initialize(attributes = {})
109
+ super(attributes.except(:time_date, :time_duration, :time_cycle))
110
+
111
+ @time_duration_type = attributes[:time_duration_type]
112
+ @time_duration = attributes[:time_duration]
113
+ end
114
+
115
+ def execute(execution)
116
+ if execution.step.is_catching?
117
+ execution.timer_expires_at = time_due
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def time_due
124
+ # Return the next time the timer is due
125
+ if time_date
126
+ return Date.parse(time_date)
127
+ elsif time_duration
128
+ return Time.zone.now + ActiveSupport::Duration.parse(time_duration)
129
+ else
130
+ return Time.zone.now # time_cycle not yet implemented
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFlow
4
+ module Bpmn
5
+ class ExtensionElements
6
+ VALID_EXTENSION_NAMESPACES = %w[zeebe]
7
+
8
+ attr_accessor :assignment_definition, :called_element, :called_decision, :form_definition, :io_mapping, :properties, :script, :subscription, :task_definition, :task_headers, :task_schedule
9
+
10
+ def initialize(attributes = {})
11
+ if attributes[:properties].present?
12
+ @properties = HashWithIndifferentAccess.new
13
+ Array.wrap(attributes[:properties][:property]).each { |property_moddle| @properties[property_moddle[:name]] = property_moddle[:value] }
14
+ end
15
+
16
+ @assignment_definition = Zeebe::AssignmentDefinition.new(attributes[:assignment_definition]) if attributes[:assignment_definition].present?
17
+ @called_element = Zeebe::CalledElement.new(attributes[:called_element]) if attributes[:called_element].present?
18
+ @called_decision = Zeebe::CalledDecision.new(attributes[:called_decision]) if attributes[:called_decision].present?
19
+ @form_definition = Zeebe::FormDefinition.new(attributes[:form_definition]) if attributes[:form_definition].present?
20
+ @io_mapping = Zeebe::IoMapping.new(attributes[:io_mapping]) if attributes[:io_mapping].present?
21
+ @script = Zeebe::Script.new(attributes[:script]) if attributes[:script].present?
22
+ @subscription = Zeebe::Subscription.new(attributes[:subscription]) if attributes[:subscription].present?
23
+ @task_definition = Zeebe::TaskDefinition.new(attributes[:task_definition]) if attributes[:task_definition].present?
24
+ @task_headers = Zeebe::TaskHeaders.new(attributes[:task_headers]) if attributes[:task_headers].present?
25
+ @task_schedule = Zeebe::TaskSchedule.new(attributes[:task_schedule]) if attributes[:task_schedule].present?
26
+ end
27
+ end
28
+ end
29
+ end