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.
@@ -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: spot_flow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Snawar92
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-11-22 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
+ - snawar92@gmail.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.7
230
+ signing_key:
231
+ specification_version: 4
232
+ summary: A BPMN workflow engine in Ruby
233
+ test_files: []