bpmn 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,360 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFlow
4
+ class Execution
5
+ attr_accessor :id, :status, :started_at, :ended_at, :variables, :tokens_in, :tokens_out, :start_event_id, :timer_expires_at, :message_names, :error_names, :condition
6
+ attr_accessor :step, :parent, :children, :context, :attached_to_id
7
+
8
+ delegate :print, to: :printer
9
+
10
+ def self.start(context:, process:, variables: {}, start_event_id: nil, parent: nil)
11
+ Execution.new(context: context, step: process, variables: variables, start_event_id: start_event_id, parent: parent).tap do |execution|
12
+ context.executions.push execution
13
+ execution.start
14
+ end
15
+ end
16
+
17
+ def self.deserialize(json, context:)
18
+ if json.is_a?(String)
19
+ attributes = JSON.parse(json)
20
+ else
21
+ attributes = json
22
+ end
23
+ Execution.from_json(attributes, context: context)
24
+ end
25
+
26
+ def self.from_json(attributes, context:)
27
+ step_id = attributes.delete("step_id")
28
+ step_type = attributes.delete("step_type")
29
+ step = step_type == "Process" ? context.process_by_id(step_id) : context.element_by_id(step_id)
30
+ child_attributes = attributes.delete("children")
31
+ Execution.new(attributes.merge(step: step, context:)).tap do |execution|
32
+ execution.children = child_attributes.map do |ca|
33
+ Execution.from_json(ca, context:).tap { |child| child.parent = execution }
34
+ end if child_attributes
35
+ end
36
+ end
37
+
38
+ def gen_uid
39
+ rand(36**8).to_s(36)
40
+ end
41
+
42
+ def initialize(attributes={})
43
+ attributes.each do |k, v|
44
+ send("#{k}=", v)
45
+ end
46
+ @id ||= gen_uid
47
+ @status ||= "activated"
48
+ @variables = @variables&.with_indifferent_access || {}.with_indifferent_access
49
+ @tokens_in ||= []
50
+ @tokens_out ||= []
51
+ @message_names ||= []
52
+ @error_names ||= []
53
+ @children ||= []
54
+ end
55
+
56
+ def started?
57
+ started_at.present?
58
+ end
59
+
60
+ def ended?
61
+ ended_at.present?
62
+ end
63
+
64
+ def activated?
65
+ status == "activated"
66
+ end
67
+
68
+ def waiting?
69
+ status == "waiting"
70
+ end
71
+
72
+ def completed?
73
+ status == "completed"
74
+ end
75
+
76
+ def terminated?
77
+ status == "terminated"
78
+ end
79
+
80
+ def execute_steps(steps)
81
+ steps.each { |step| execute_step(step) }
82
+ end
83
+
84
+ def execute_step(step, attached_to: nil, sequence_flow: nil)
85
+ child_execution = children.find { |child| child.step.id == step.id }
86
+ child_execution = Execution.new(context: context, step: step, parent: self, attached_to_id: attached_to&.id).tap { |ce| children.push ce } unless child_execution
87
+ child_execution.tokens_in += [sequence_flow.id] if sequence_flow
88
+ child_execution.start
89
+ end
90
+
91
+ def invoke_listeners(type, sequence_flow = nil)
92
+ context.listeners.each { |listener| listener[type].call(self, sequence_flow) if listener[type] }
93
+ end
94
+
95
+ def start
96
+ @status = "started"
97
+ @started_at = Time.zone.now
98
+ map_input_variables if step&.input_mappings&.present?
99
+ context.notify_listener(:execution_started, execution: self)
100
+ step.attachments.each { |attachment| parent.execute_step(attachment, attached_to: self) } if step.is_a?(SpotFlow::Bpmn::Activity)
101
+ continue
102
+ end
103
+
104
+ def continue
105
+ step.execute(self)
106
+ end
107
+
108
+ def wait
109
+ @status = "waiting"
110
+ context.notify_listener(:execution_waited, execution: self)
111
+ end
112
+
113
+ def terminate
114
+ @status = "terminated"
115
+ self.end
116
+ end
117
+
118
+ def end(notify_parent = false)
119
+ @status = "completed" unless status == "terminated"
120
+ map_output_variables if step&.output_mappings&.present?
121
+ parent.variables.merge!(variables) if parent && variables.present?
122
+ @ended_at = Time.zone.now
123
+ context.notify_listener(:execution_ended, execution: self)
124
+ children.each { |child| child.terminate unless child.ended? }
125
+ parent.children.each { |child| child.terminate if child.attached_to == self && child.waiting? } if parent
126
+ parent.has_ended(self) if parent && notify_parent
127
+ end
128
+
129
+ def take_all(sequence_flows)
130
+ sequence_flows.each { |sequence_flow| take(sequence_flow) }
131
+ end
132
+
133
+ def take(sequence_flow)
134
+ to_step = sequence_flow.target
135
+ tokens_out.push sequence_flow.id
136
+ tokens_out.uniq!
137
+ context.notify_listener(:flow_taken, execution: self, sequence_flow: sequence_flow)
138
+ parent.execute_step(to_step, sequence_flow: sequence_flow)
139
+ end
140
+
141
+ def signal(result = nil)
142
+ @variables.merge!(result_to_variables(result)) if result.present?
143
+ raise ExecutionError.new("Cannot signal a step execution that has ended.") if ended?
144
+ step.signal(self)
145
+ end
146
+
147
+ def throw_message(message_name, variables: {})
148
+ waiting_children.each do |child|
149
+ step = child.step
150
+ if step.is_a?(SpotFlow::Bpmn::Event) && step.message_event_definitions.any? { |message_event_definition| message_event_definition.message_name == message_name }
151
+ child.signal(variables)
152
+ break
153
+ end
154
+ end
155
+ context.notify_listener(:message_thrown, execution: self, message_name: message_name)
156
+ end
157
+
158
+ def throw_error(error_name, variables: {})
159
+ waiting_children.each do |child|
160
+ step = child.step
161
+ if step.is_a?(SpotFlow::Bpmn::Event) && step.error_event_definitions.any? { |error_event_definition| error_event_definition.error_name == error_name }
162
+ child.signal(variables)
163
+ break
164
+ end
165
+ end
166
+ context.notify_listener(:error_thrown, execution: self, error_name: error_name)
167
+ end
168
+
169
+ def check_expired_timers
170
+ waiting_children.each { |child| child.signal if child.timer_expires_at.present? && Time.zone.now > child.timer_expires_at }
171
+ end
172
+
173
+ def evaluate_condition(condition)
174
+ evaluate_expression(condition.delete_prefix("=")) == true
175
+ end
176
+
177
+ def evaluate_expression(expression, variables: parent&.variables || {}.with_indifferent_access)
178
+ SpotFeel.evaluate(expression.delete_prefix("="), variables:)
179
+ end
180
+
181
+ def run_automated_tasks
182
+ waiting_automated_tasks.each { |child| child.run }
183
+ end
184
+
185
+ def run
186
+ return unless step.is_automated?
187
+
188
+ result = step.run(self)
189
+
190
+ if result.present?
191
+ signal(result)
192
+ else
193
+ wait
194
+ end
195
+ end
196
+
197
+ def call(process)
198
+ execute_step(process, attached_to: self)
199
+ end
200
+
201
+ #
202
+ # Called by the child step executors when they have ended
203
+ #
204
+ def has_ended(_child)
205
+ step.leave(self) if step.is_a?(SpotFlow::Bpmn::SubProcess) || step.is_a?(SpotFlow::Bpmn::CallActivity)
206
+ self.end(true)
207
+ end
208
+
209
+ def attached_to
210
+ @attached_to ||= parent.children.find { |child| child.id == attached_to_id } if parent
211
+ end
212
+
213
+ def child_by_step_id(id)
214
+ children.find { |child| child.step.id == id }
215
+ end
216
+
217
+ def waiting_children
218
+ children.filter { |child| child.waiting? }
219
+ end
220
+
221
+ def waiting_tasks
222
+ waiting_children.select { |child| child.step.is_a?(SpotFlow::Bpmn::Task) }
223
+ end
224
+
225
+ def waiting_automated_tasks
226
+ waiting_tasks.select { |child| child.step.is_automated? }
227
+ end
228
+
229
+ def tokens(active_tokens = [])
230
+ children.each do |child|
231
+ active_tokens = active_tokens + child.tokens_out
232
+ active_tokens = active_tokens - child.tokens_in if child.ended?
233
+ active_tokens = active_tokens + child.tokens(active_tokens)
234
+ end
235
+ active_tokens.uniq
236
+ end
237
+
238
+ def serialize(...)
239
+ to_json(...)
240
+ end
241
+
242
+ def as_json(_options = {})
243
+ {
244
+ id: id,
245
+ step_id: step&.id,
246
+ step_type: step&.class&.name&.demodulize,
247
+ attached_to_id: attached_to_id,
248
+ status: status,
249
+ started_at: started_at,
250
+ ended_at: ended_at,
251
+ variables: variables.as_json,
252
+ tokens_in: tokens_in,
253
+ tokens_out: tokens_out,
254
+ message_names: message_names,
255
+ error_names: error_names,
256
+ timer_expires_at: timer_expires_at,
257
+ condition: condition,
258
+ children: children.map { |child| child.as_json },
259
+ }.transform_values(&:presence).compact
260
+ end
261
+
262
+ def inspect
263
+ parts = ["#<Execution @id=#{id.inspect}"]
264
+ parts << "@step_type=#{step&.class&.name&.demodulize}" if step
265
+ parts << "@step_id=#{step.id.inspect}" if step
266
+ parts << "@status=#{status.inspect}" if status
267
+ parts << "@started_at=#{started_at.inspect}" if started_at
268
+ parts << "@ended_at=#{ended_at.inspect}" if ended_at
269
+ parts << "@attached_to_id=#{attached_to_id.inspect}" if attached_to_id
270
+ parts << "@variables=#{variables.inspect}" if variables.present?
271
+ # parts << "@tokens_in=#{tokens_in.inspect}" if tokens_in.present?
272
+ # parts << "@tokens_out=#{tokens_out.inspect}" if tokens_out.present?
273
+ parts << "@message_names=#{message_names.inspect}" if message_names.present?
274
+ parts << "@error_names=#{error_names.inspect}" if error_names.present?
275
+ parts << "@timer_expires_at=#{timer_expires_at.inspect}" if timer_expires_at
276
+ parts << "@condition=#{condition.inspect}" if condition
277
+ parts << "@children=#{children.inspect}" if children.present?
278
+ parts.join(" ") + ">"
279
+ end
280
+
281
+ private
282
+
283
+ def map_input_variables
284
+ return unless step&.input_mappings&.present?
285
+ step.input_mappings.each do |parameter|
286
+ variables[parameter.target] = evaluate_expression(parameter.source)
287
+ end
288
+ end
289
+
290
+ def map_output_variables
291
+ return unless step&.output_mappings&.present?
292
+ step.output_mappings.each do |parameter|
293
+ variables[parameter.target] = evaluate_expression(parameter.source)
294
+ end
295
+ end
296
+
297
+ def result_to_variables(result)
298
+ if step.respond_to?(:result_variable) && step.result_variable
299
+ return { "#{step.result_variable}": result }
300
+ else
301
+ if result.is_a? Hash
302
+ result
303
+ else
304
+ {}.tap { |h| h[step.id.underscore] = result }
305
+ end
306
+ end
307
+ end
308
+
309
+ def printer
310
+ @printer ||= ExecutionPrinter.new(self)
311
+ end
312
+ end
313
+
314
+ class ExecutionError < StandardError
315
+ attr_reader :execution
316
+
317
+ def initialize(msg, execution: nil)
318
+ @execution = execution
319
+ super(msg)
320
+ end
321
+ end
322
+
323
+ class ExecutionPrinter
324
+ attr_accessor :execution
325
+
326
+ def initialize(execution)
327
+ @execution = execution
328
+ end
329
+
330
+ def print
331
+ puts
332
+ puts "#{execution.step.id} #{execution.status} * #{execution.tokens.join(', ')}"
333
+ print_variables unless execution.variables.empty?
334
+ print_children
335
+ puts
336
+ end
337
+
338
+ def print_children
339
+ puts
340
+ execution.children.each_with_index do |child, index|
341
+ print_child(child, index)
342
+ end
343
+ end
344
+
345
+ def print_child(child, index)
346
+ str = "#{index} #{child.step.class.name.demodulize} #{child.step.id}: #{child.status} #{JSON.pretty_generate(child.variables, { indent: '', object_nl: ' ' }) unless child.variables.empty? }".strip
347
+ str = "#{str} * in: #{child.tokens_in.join(', ')}" if child.tokens_in.present?
348
+ str = "#{str} * out: #{child.tokens_out.join(', ')}" if child.tokens_out.present?
349
+ puts str
350
+ child.children.each_with_index do |grandchild, grandindex|
351
+ print_child(grandchild, "#{index}.#{grandindex}")
352
+ end
353
+ end
354
+
355
+ def print_variables
356
+ puts
357
+ puts JSON.pretty_generate(execution.variables)
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFlow
4
+ VERSION = "0.0.1"
5
+ end
data/lib/spot_flow.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spot_flow/version"
4
+
5
+ require "active_support"
6
+ require "active_support/time"
7
+ require "active_support/core_ext/hash"
8
+ require "active_support/core_ext/object/json"
9
+ require "active_support/configurable"
10
+ require "active_model"
11
+
12
+ require "spot_feel"
13
+
14
+ require "spot_flow/bpmn"
15
+ require "spot_flow/context"
16
+ require "spot_flow/execution"
17
+
18
+ module SpotFlow
19
+ include ActiveSupport::Configurable
20
+
21
+ #
22
+ # Entry point for starting a process execution.
23
+ #
24
+ def self.new(sources = [])
25
+ Context.new(sources)
26
+ end
27
+
28
+ #
29
+ # Entry point for continuing a process execution.
30
+ #
31
+ def self.restore(sources = [], execution_state:)
32
+ Context.new(sources).restore(execution_state)
33
+ end
34
+
35
+ #
36
+ # Extract processes from a BMPN XML file.
37
+ #
38
+ def self.processes_from_xml(xml)
39
+ Bpmn::Definitions.from_xml(xml)&.processes || []
40
+ end
41
+
42
+ #
43
+ # Extract decisions from a DMN XML file.
44
+ #
45
+ def self.decisions_from_xml(xml)
46
+ definitions = SpotFeel.definitions_from_xml(xml)
47
+ definitions.decisions
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,233 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bpmn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Connected Bits
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-12-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.2.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: xmlhasher
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.7
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest-spec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest-reporters
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest-focus
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-performance
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: solargraph
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Spot Flow is a workflow gem for Rails applications based on the [bpmn](https://www.bpmn.org)
182
+ standard. It executes business processes and rules defined in a modeler.
183
+ email:
184
+ - info@connectedbits.com
185
+ executables: []
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - MIT-LICENSE
190
+ - README.md
191
+ - Rakefile
192
+ - lib/spot_flow.rb
193
+ - lib/spot_flow/bpmn.rb
194
+ - lib/spot_flow/bpmn/definitions.rb
195
+ - lib/spot_flow/bpmn/element.rb
196
+ - lib/spot_flow/bpmn/event.rb
197
+ - lib/spot_flow/bpmn/event_definition.rb
198
+ - lib/spot_flow/bpmn/extension_elements.rb
199
+ - lib/spot_flow/bpmn/extensions.rb
200
+ - lib/spot_flow/bpmn/flow.rb
201
+ - lib/spot_flow/bpmn/gateway.rb
202
+ - lib/spot_flow/bpmn/process.rb
203
+ - lib/spot_flow/bpmn/step.rb
204
+ - lib/spot_flow/bpmn/task.rb
205
+ - lib/spot_flow/context.rb
206
+ - lib/spot_flow/execution.rb
207
+ - lib/spot_flow/version.rb
208
+ homepage: https://www.connectedbits.com
209
+ licenses:
210
+ - MIT
211
+ metadata:
212
+ homepage_uri: https://www.connectedbits.com
213
+ source_code_uri: https://github.com/connectedbits/process_pilot
214
+ post_install_message:
215
+ rdoc_options: []
216
+ require_paths:
217
+ - lib
218
+ required_ruby_version: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '3.1'
223
+ required_rubygems_version: !ruby/object:Gem::Requirement
224
+ requirements:
225
+ - - ">="
226
+ - !ruby/object:Gem::Version
227
+ version: '0'
228
+ requirements: []
229
+ rubygems_version: 3.3.21
230
+ signing_key:
231
+ specification_version: 4
232
+ summary: A BPMN workflow engine in Ruby
233
+ test_files: []