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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +187 -0
- data/Rakefile +11 -0
- data/lib/spot_flow/bpmn/definitions.rb +55 -0
- data/lib/spot_flow/bpmn/element.rb +44 -0
- data/lib/spot_flow/bpmn/event.rb +195 -0
- data/lib/spot_flow/bpmn/event_definition.rb +135 -0
- data/lib/spot_flow/bpmn/extension_elements.rb +29 -0
- data/lib/spot_flow/bpmn/extensions.rb +77 -0
- data/lib/spot_flow/bpmn/flow.rb +47 -0
- data/lib/spot_flow/bpmn/gateway.rb +85 -0
- data/lib/spot_flow/bpmn/process.rb +179 -0
- data/lib/spot_flow/bpmn/step.rb +58 -0
- data/lib/spot_flow/bpmn/task.rb +128 -0
- data/lib/spot_flow/bpmn.rb +18 -0
- data/lib/spot_flow/context.rb +108 -0
- data/lib/spot_flow/execution.rb +360 -0
- data/lib/spot_flow/version.rb +5 -0
- data/lib/spot_flow.rb +49 -0
- metadata +233 -0
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SpotFlow
         | 
| 4 | 
            +
              module Bpmn
         | 
| 5 | 
            +
                class Extension
         | 
| 6 | 
            +
                  include ActiveModel::Model
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              module Zeebe
         | 
| 11 | 
            +
                class AssignmentDefinition < Bpmn::Extension
         | 
| 12 | 
            +
                  attr_accessor :assignee, :candidate_groups, :candidate_users
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                class CalledElement < Bpmn::Extension
         | 
| 16 | 
            +
                  attr_accessor :process_id, :propagate_all_child_variables, :propagate_all_parent_variables
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def initialize(attributes = {})
         | 
| 19 | 
            +
                    super(attributes.except(:propagate_all_child_variables))
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    @propagate_all_parent_variables = true
         | 
| 22 | 
            +
                    @propagate_all_parent_variables = attributes[:propagate_all_parent_variables] == "true" if attributes[:propagate_all_parent_variables].present?
         | 
| 23 | 
            +
                    @propagate_all_child_variables = attributes[:propagate_all_child_variables] == "true"
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                class CalledDecision < Bpmn::Extension
         | 
| 28 | 
            +
                  attr_accessor :decision_id, :result_variable
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                class FormDefinition < Bpmn::Extension
         | 
| 32 | 
            +
                  attr_accessor :form_key
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                class IoMapping < Bpmn::Extension
         | 
| 36 | 
            +
                  attr_reader :inputs, :outputs
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def initialize(attributes = {})
         | 
| 39 | 
            +
                    super(attributes.except(:input, :output))
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    @inputs = Array.wrap(attributes[:input]).map { |atts| Parameter.new(atts) } if attributes[:input].present?
         | 
| 42 | 
            +
                    @outputs = Array.wrap(attributes[:output]).map { |atts| Parameter.new(atts) } if attributes[:output].present?
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                class Parameter < Bpmn::Extension
         | 
| 47 | 
            +
                  attr_accessor :source, :target
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                class Script < Bpmn::Extension
         | 
| 51 | 
            +
                  attr_accessor :expression, :result_variable
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                class Subscription < Bpmn::Extension
         | 
| 55 | 
            +
                  attr_accessor :correlation_key
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                class TaskDefinition < Bpmn::Extension
         | 
| 59 | 
            +
                  attr_accessor :type, :retries
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                class TaskHeaders < Bpmn::Extension
         | 
| 63 | 
            +
                  attr_accessor :headers
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def initialize(attributes = {})
         | 
| 66 | 
            +
                    super(attributes.except(:header))
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    @headers = HashWithIndifferentAccess.new
         | 
| 69 | 
            +
                    Array.wrap(attributes[:header]).each { |header| @headers[header[:key]] = header[:value] }
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                class TaskSchedule < Bpmn::Extension
         | 
| 74 | 
            +
                  attr_accessor :due_date, :follow_up_date
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
            end
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SpotFlow
         | 
| 4 | 
            +
              module Bpmn
         | 
| 5 | 
            +
                class Flow < Element
         | 
| 6 | 
            +
                  attr_accessor :source_ref, :target_ref
         | 
| 7 | 
            +
                  attr_accessor :source, :target
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def initialize(attributes = {})
         | 
| 10 | 
            +
                    super(attributes.except(:source_ref, :target_ref))
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    @source_ref = attributes[:source_ref]
         | 
| 13 | 
            +
                    @target_ref = attributes[:target_ref]
         | 
| 14 | 
            +
                    @source = nil
         | 
| 15 | 
            +
                    @target = nil
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def inspect
         | 
| 19 | 
            +
                    parts = ["#<#{self.class.name.gsub(/SpotFlow::/, "")} @id=#{id.inspect}"]
         | 
| 20 | 
            +
                    parts << "@source_ref=#{source_ref.inspect}" if source_ref
         | 
| 21 | 
            +
                    parts << "@target_ref=#{target_ref.inspect}" if target_ref
         | 
| 22 | 
            +
                    parts.join(" ") + ">"
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                class Association < Flow
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                class SequenceFlow < Flow
         | 
| 30 | 
            +
                  attr_accessor :condition
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def initialize(attributes = {})
         | 
| 33 | 
            +
                    super(attributes.except(:condition))
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    @condition = attributes[:condition_expression]
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def evaluate(execution)
         | 
| 39 | 
            +
                    return true unless condition
         | 
| 40 | 
            +
                    execution.evaluate_condition(condition)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                class TextAnnotation < Flow
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
            end
         | 
| @@ -0,0 +1,85 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SpotFlow
         | 
| 4 | 
            +
              module Bpmn
         | 
| 5 | 
            +
                class Gateway < Step
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def execute(execution)
         | 
| 8 | 
            +
                    if converging?
         | 
| 9 | 
            +
                      if is_enabled?(execution)
         | 
| 10 | 
            +
                        return leave(execution)
         | 
| 11 | 
            +
                      else
         | 
| 12 | 
            +
                        execution.wait
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
                    else
         | 
| 15 | 
            +
                      return leave(execution)
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  #
         | 
| 20 | 
            +
                  # Algorithm from https://researcher.watson.ibm.com/researcher/files/zurich-hvo/bpm2010-1.pdf
         | 
| 21 | 
            +
                  #
         | 
| 22 | 
            +
                  def is_enabled?(execution)
         | 
| 23 | 
            +
                    filled = []
         | 
| 24 | 
            +
                    empty = []
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    incoming.each { |flow| execution.tokens_in.include?(flow.id) ? filled.push(flow) : empty.push(flow) }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    # Filled slots don't need to be searched for tokens
         | 
| 29 | 
            +
                    index = 0
         | 
| 30 | 
            +
                    while (index < filled.length)
         | 
| 31 | 
            +
                      current_flow = filled[index]
         | 
| 32 | 
            +
                      current_flow.source.incoming.each do |incoming_flow|
         | 
| 33 | 
            +
                        filled.push(incoming_flow) unless filled.include?(incoming_flow) || incoming_flow.target == self
         | 
| 34 | 
            +
                      end
         | 
| 35 | 
            +
                      index = index + 1
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    # Empty slots need to be searched for tokens
         | 
| 39 | 
            +
                    index = 0
         | 
| 40 | 
            +
                    while (index < empty.length)
         | 
| 41 | 
            +
                      current_flow = empty[index]
         | 
| 42 | 
            +
                      current_flow.source.incoming.each do |incoming_flow|
         | 
| 43 | 
            +
                        empty.push(incoming_flow) unless filled.include?(incoming_flow) || empty.include?(incoming_flow) || incoming_flow.target == self
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
                      index = index + 1
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    empty_ids = empty.map { |g| g.id }
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    # If there are empty slots with tokens we need to wait
         | 
| 51 | 
            +
                    return false if (empty_ids & execution.parent.tokens).length > 0
         | 
| 52 | 
            +
                    return true
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                class ExclusiveGateway < Gateway
         | 
| 57 | 
            +
                  # RULE: Only one flow is taken
         | 
| 58 | 
            +
                  def outgoing_flows(step_execution)
         | 
| 59 | 
            +
                    flows = super
         | 
| 60 | 
            +
                    return [flows.first]
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                class ParallelGateway < Gateway
         | 
| 65 | 
            +
                  # RULE: All flows are taken
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                class InclusiveGateway < Gateway
         | 
| 69 | 
            +
                  # RULE: All valid flows are taken
         | 
| 70 | 
            +
                  # NOTE: Camunda 8 only support diverging but not converging inclusive gateways
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                class EventBasedGateway < Gateway
         | 
| 74 | 
            +
                  #
         | 
| 75 | 
            +
                  # RULE: when an event created from an event gateway is caught,
         | 
| 76 | 
            +
                  # all other waiting events must be canceled.
         | 
| 77 | 
            +
                  #
         | 
| 78 | 
            +
                  def cancel_waiting_events(execution)
         | 
| 79 | 
            +
                    execution.targets.each do |target_execution|
         | 
| 80 | 
            +
                      target_execution.terminate unless target_execution.ended?
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
              end
         | 
| 85 | 
            +
            end
         | 
| @@ -0,0 +1,179 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SpotFlow
         | 
| 4 | 
            +
              module Bpmn
         | 
| 5 | 
            +
                class Process < Step
         | 
| 6 | 
            +
                  attr_accessor :is_executable
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  attr_accessor :start_events, :end_events, :intermediate_catch_events, :intermediate_throw_events, :boundary_events
         | 
| 9 | 
            +
                  attr_accessor :tasks, :user_tasks, :service_tasks, :script_tasks, :send_tasks, :receive_tasks, :manual_tasks, :business_rule_tasks, :call_activities
         | 
| 10 | 
            +
                  attr_accessor :sub_processes, :ad_hoc_sub_processes
         | 
| 11 | 
            +
                  attr_accessor :sequence_flows, :message_flows, :associations, :data_associations, :data_inputs, :data_outputs
         | 
| 12 | 
            +
                  attr_accessor :data_objects, :data_stores, :data_stores_references
         | 
| 13 | 
            +
                  attr_accessor :gateways, :exclusive_gateways, :parallel_gateways, :inclusive_gateways, :event_based_gateways, :complex_gateways
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  attr_accessor :parent
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def initialize(attributes = {})
         | 
| 18 | 
            +
                    super(attributes.slice(:id, :name, :extension_elements, :incoming, :outgoing, :default))
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    @is_executable = attributes[:is_executable] == ("true" || true)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    @start_events = Array.wrap(attributes[:start_event]).map { |se| StartEvent.new(se) }
         | 
| 23 | 
            +
                    @end_events = Array.wrap(attributes[:end_event]).map { |ee| EndEvent.new(ee) }
         | 
| 24 | 
            +
                    @intermediate_catch_events = Array.wrap(attributes[:intermediate_catch_event]).map { |ice| IntermediateCatchEvent.new(ice) }
         | 
| 25 | 
            +
                    @intermediate_throw_events = Array.wrap(attributes[:intermediate_throw_event]).map { |ite| IntermediateThrowEvent.new(ite) }
         | 
| 26 | 
            +
                    @boundary_events = Array.wrap(attributes[:boundary_event]).map { |be| BoundaryEvent.new(be) }
         | 
| 27 | 
            +
                    @tasks = Array.wrap(attributes[:task]).map { |t| Task.new(t) }
         | 
| 28 | 
            +
                    @user_tasks = Array.wrap(attributes[:user_task]).map { |ut| UserTask.new(ut) }
         | 
| 29 | 
            +
                    @service_tasks = Array.wrap(attributes[:service_task]).map { |st| ServiceTask.new(st) }
         | 
| 30 | 
            +
                    @script_tasks = Array.wrap(attributes[:script_task]).map { |st| ScriptTask.new(st) }
         | 
| 31 | 
            +
                    @business_rule_tasks = Array.wrap(attributes[:business_rule_task]).map { |brt| BusinessRuleTask.new(brt) }
         | 
| 32 | 
            +
                    @call_activities = Array.wrap(attributes[:call_activity]).map { |ca| CallActivity.new(ca) }
         | 
| 33 | 
            +
                    @sub_processes = Array.wrap(attributes[:sub_process]).map { |sp| SubProcess.new(sp) }
         | 
| 34 | 
            +
                    @ad_hoc_sub_processes = Array.wrap(attributes[:ad_hoc_sub_processe]).map { |ahsp| AdHocSubProcess.new(ahsp) }
         | 
| 35 | 
            +
                    @exclusive_gateways = Array.wrap(attributes[:exclusive_gateway]).map { |eg| ExclusiveGateway.new(eg) }
         | 
| 36 | 
            +
                    @parallel_gateways = Array.wrap(attributes[:parallel_gateway]).map { |pg| ParallelGateway.new(pg) }
         | 
| 37 | 
            +
                    @inclusive_gateways = Array.wrap(attributes[:inclusive_gateway]).map { |ig| InclusiveGateway.new(ig) }
         | 
| 38 | 
            +
                    @event_based_gateways = Array.wrap(attributes[:event_based_gateway]).map { |ebg| EventBasedGateway.new(ebg) }
         | 
| 39 | 
            +
                    @sequence_flows = Array.wrap(attributes[:sequence_flow]).map { |sf| SequenceFlow.new(sf) }
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def elements
         | 
| 43 | 
            +
                    @elements ||= {}.tap do |elements|
         | 
| 44 | 
            +
                      elements.merge!(start_events.index_by(&:id))
         | 
| 45 | 
            +
                      elements.merge!(end_events.index_by(&:id))
         | 
| 46 | 
            +
                      elements.merge!(intermediate_catch_events.index_by(&:id))
         | 
| 47 | 
            +
                      elements.merge!(intermediate_throw_events.index_by(&:id))
         | 
| 48 | 
            +
                      elements.merge!(boundary_events.index_by(&:id))
         | 
| 49 | 
            +
                      elements.merge!(tasks.index_by(&:id))
         | 
| 50 | 
            +
                      elements.merge!(user_tasks.index_by(&:id))
         | 
| 51 | 
            +
                      elements.merge!(service_tasks.index_by(&:id))
         | 
| 52 | 
            +
                      elements.merge!(script_tasks.index_by(&:id))
         | 
| 53 | 
            +
                      elements.merge!(business_rule_tasks.index_by(&:id))
         | 
| 54 | 
            +
                      elements.merge!(call_activities.index_by(&:id))
         | 
| 55 | 
            +
                      elements.merge!(sub_processes.index_by(&:id))
         | 
| 56 | 
            +
                      elements.merge!(ad_hoc_sub_processes.index_by(&:id))
         | 
| 57 | 
            +
                      elements.merge!(exclusive_gateways.index_by(&:id))
         | 
| 58 | 
            +
                      elements.merge!(parallel_gateways.index_by(&:id))
         | 
| 59 | 
            +
                      elements.merge!(inclusive_gateways.index_by(&:id))
         | 
| 60 | 
            +
                      elements.merge!(event_based_gateways.index_by(&:id))
         | 
| 61 | 
            +
                      elements.merge!(sequence_flows.index_by(&:id))
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def wire_references(definitions)
         | 
| 66 | 
            +
                    elements.values.each do |element|
         | 
| 67 | 
            +
                      if element.is_a?(Step)
         | 
| 68 | 
            +
                        element.incoming = element.incoming.map { |id| element_by_id(id) }
         | 
| 69 | 
            +
                        element.outgoing = element.outgoing.map { |id| element_by_id(id) }
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                        if element.is_a?(Event)
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                          element.event_definitions.each do |event_definition|
         | 
| 74 | 
            +
                            if event_definition.is_a?(MessageEventDefinition)
         | 
| 75 | 
            +
                              event_definition.message = definitions.message_by_id(event_definition.message_ref)
         | 
| 76 | 
            +
                            elsif event_definition.is_a?(SignalEventDefinition)
         | 
| 77 | 
            +
                              event_definition.signal = definitions.signal_by_id(event_definition.signal_ref)
         | 
| 78 | 
            +
                            elsif event_definition.is_a?(ErrorEventDefinition)
         | 
| 79 | 
            +
                              event_definition.error = definitions.error_by_id(event_definition.error_ref)
         | 
| 80 | 
            +
                            end
         | 
| 81 | 
            +
                          end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                          if element.is_a?(BoundaryEvent)
         | 
| 84 | 
            +
                            host_element = element_by_id(element.attached_to_ref)
         | 
| 85 | 
            +
                            host_element.attachments << element
         | 
| 86 | 
            +
                            element.attached_to = host_element
         | 
| 87 | 
            +
                          end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                        end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                        if element.is_a?(Gateway)
         | 
| 92 | 
            +
                          element.default = element_by_id(element.default_ref)
         | 
| 93 | 
            +
                        end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                        if element.is_a?(SubProcess)
         | 
| 96 | 
            +
                          element.wire_references(definitions)
         | 
| 97 | 
            +
                        end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                      elsif element.is_a?(SequenceFlow)
         | 
| 100 | 
            +
                        element.source = element_by_id(element.source_ref)
         | 
| 101 | 
            +
                        element.target = element_by_id(element.target_ref)
         | 
| 102 | 
            +
                      end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                      # Not handled participant process ref
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  def element_by_id(id)
         | 
| 109 | 
            +
                    elements[id]
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  def elements_by_type(type)
         | 
| 113 | 
            +
                    elements.select { |e| e.class == type }
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  def default_start_event
         | 
| 117 | 
            +
                    start_events.find { |se| se.event_definitions.empty? }
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  def execute(execution)
         | 
| 121 | 
            +
                    start_event = execution.start_event_id ? element_by_id(execution.start_event_id) : default_start_event
         | 
| 122 | 
            +
                    raise ExecutionErrorNew.new("Process must have at least one start event.") if start_event.blank?
         | 
| 123 | 
            +
                    execution.execute_step(start_event)
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  def inspect
         | 
| 127 | 
            +
                    parts = ["#<#{self.class.name.gsub(/SpotFlow::/, "")} @id=#{id.inspect} @name=#{name.inspect} @is_executable=#{is_executable.inspect}"]
         | 
| 128 | 
            +
                    parts << "@parent=#{parent.inspect}" if parent
         | 
| 129 | 
            +
                    event_attrs = (start_events + intermediate_catch_events + intermediate_throw_events + boundary_events + end_events).compact
         | 
| 130 | 
            +
                    parts << "@events=#{event_attrs.inspect}" unless event_attrs.blank?
         | 
| 131 | 
            +
                    activity_attrs = (tasks + user_tasks + service_tasks + script_tasks + business_rule_tasks + call_activities).compact
         | 
| 132 | 
            +
                    parts << "@activities=#{activity_attrs.inspect}" unless activity_attrs.blank?
         | 
| 133 | 
            +
                    gateway_attrs = (exclusive_gateways + parallel_gateways + inclusive_gateways + event_based_gateways).compact
         | 
| 134 | 
            +
                    parts << "@gateways=#{gateway_attrs.inspect}" unless gateway_attrs.blank?
         | 
| 135 | 
            +
                    sub_process_attrs = (sub_processes + ad_hoc_sub_processes).compact
         | 
| 136 | 
            +
                    parts << "@sub_processes=#{sub_process_attrs.inspect}" unless sub_process_attrs.blank? 
         | 
| 137 | 
            +
                    parts << "@sequence_flows=#{sequence_flows.inspect}" unless sequence_flows.blank?
         | 
| 138 | 
            +
                    parts.join(" ") + ">"
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                class SubProcess < Process
         | 
| 143 | 
            +
                  attr_accessor :triggered_by_event
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                  def initialize(attributes = {})
         | 
| 146 | 
            +
                    super(attributes.except(:triggered_by_event))
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                    @is_executable = false
         | 
| 149 | 
            +
                    @sub_processes = []
         | 
| 150 | 
            +
                    @triggered_by_event = attributes[:triggered_by_event]
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                  def execution_ended(execution)
         | 
| 154 | 
            +
                    leave(execution)
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                class AdHocSubProcess < SubProcess
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                class CallActivity < Activity
         | 
| 163 | 
            +
                  attr_reader :process_id
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                  def execute(execution)
         | 
| 166 | 
            +
                    if extension_elements&.called_element&.process_id&.start_with?("=")
         | 
| 167 | 
            +
                      @process_id = SpotFeel.evaluate(extension_elements&.called_element&.process_id, variables: execution.variables)
         | 
| 168 | 
            +
                    else
         | 
| 169 | 
            +
                      @process_id = extension_elements&.called_element&.process_id
         | 
| 170 | 
            +
                    end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                    execution.wait
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                    process = execution.context.process_by_id(@process_id) if @process_id
         | 
| 175 | 
            +
                    execution.execute_step(process.default_start_event) if process
         | 
| 176 | 
            +
                  end
         | 
| 177 | 
            +
                end
         | 
| 178 | 
            +
              end
         | 
| 179 | 
            +
            end
         | 
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SpotFlow
         | 
| 4 | 
            +
              module Bpmn
         | 
| 5 | 
            +
                class Step < Element
         | 
| 6 | 
            +
                  attr_accessor :incoming, :outgoing, :default, :default_ref
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(attributes = {})
         | 
| 9 | 
            +
                    super(attributes.except(:incoming, :outgoing, :default))
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    @incoming = Array.wrap(attributes[:incoming]) || []
         | 
| 12 | 
            +
                    @outgoing = Array.wrap(attributes[:outgoing]) || []
         | 
| 13 | 
            +
                    @default_ref = attributes[:default]
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def diverging?
         | 
| 17 | 
            +
                    outgoing.length > 1
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def converging?
         | 
| 21 | 
            +
                    incoming.length > 1
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def leave(execution)
         | 
| 25 | 
            +
                    execution.end(false)
         | 
| 26 | 
            +
                    execution.take_all(outgoing_flows(execution))
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def outgoing_flows(execution)
         | 
| 30 | 
            +
                    flows = []
         | 
| 31 | 
            +
                    outgoing.each do |flow|
         | 
| 32 | 
            +
                      result = flow.evaluate(execution) unless default&.id == flow.id
         | 
| 33 | 
            +
                      flows.push flow if result
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                    flows = [default] if flows.empty? && default
         | 
| 36 | 
            +
                    return flows
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def input_mappings
         | 
| 40 | 
            +
                    extension_elements&.io_mapping&.inputs || []
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def output_mappings
         | 
| 44 | 
            +
                    extension_elements&.io_mapping&.outputs || []
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                class Activity < Step
         | 
| 49 | 
            +
                  attr_accessor :attachments
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def initialize(attributes = {})
         | 
| 52 | 
            +
                    super(attributes.except(:attachments))
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    @attachments = []
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,128 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SpotFlow
         | 
| 4 | 
            +
              module Bpmn
         | 
| 5 | 
            +
                class Task < Activity
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  def is_automated?
         | 
| 8 | 
            +
                    false
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def is_manual?
         | 
| 12 | 
            +
                    true
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def execute(execution)
         | 
| 16 | 
            +
                    execution.wait
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def signal(execution)
         | 
| 20 | 
            +
                    leave(execution)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def result_to_variables(result)
         | 
| 24 | 
            +
                    if result_variable
         | 
| 25 | 
            +
                      return { "#{result_variable}": result }
         | 
| 26 | 
            +
                    else
         | 
| 27 | 
            +
                      if result.is_a? Hash
         | 
| 28 | 
            +
                        result
         | 
| 29 | 
            +
                      else
         | 
| 30 | 
            +
                        {}.tap { |h| h[id.underscore] = result }
         | 
| 31 | 
            +
                      end
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                class UserTask < Task
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def form_key
         | 
| 39 | 
            +
                    extension_elements&.form_definition&.form_key
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def assignee
         | 
| 43 | 
            +
                    extension_elements&.assignment_definition&.assignee
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def candidate_groups
         | 
| 47 | 
            +
                    extension_elements&.assignment_definition&.candidate_groups
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def candidate_users
         | 
| 51 | 
            +
                    extension_elements&.assignment_definition&.candidate_users
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def due_date
         | 
| 55 | 
            +
                    extension_elements&.task_schedule&.due_date
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def follow_up_date
         | 
| 59 | 
            +
                    extension_elements&.task_schedule&.follow_up_date
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                class ServiceTask < Task
         | 
| 64 | 
            +
                  attr_accessor :service
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def is_automated?
         | 
| 67 | 
            +
                    true
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def is_manual?
         | 
| 71 | 
            +
                    false
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  def execute(execution)
         | 
| 75 | 
            +
                    execution.wait
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  def task_type
         | 
| 79 | 
            +
                    extension_elements&.task_definition&.type
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  def task_retries
         | 
| 83 | 
            +
                    extension_elements&.task_definition&.retries
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  def headers
         | 
| 87 | 
            +
                    extension_elements&.task_headers&.headers
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def run(execution)
         | 
| 91 | 
            +
                    if defined?(task_type)
         | 
| 92 | 
            +
                      klass = task_type.constantize
         | 
| 93 | 
            +
                      klass.new.call(execution.parent.variables, headers || {})
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                class ScriptTask < ServiceTask
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def script
         | 
| 101 | 
            +
                    extension_elements&.script&.expression
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  def result_variable
         | 
| 105 | 
            +
                    extension_elements&.script&.result_variable
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  def run(execution)
         | 
| 109 | 
            +
                    SpotFeel.evaluate(script.delete_prefix("="), variables: execution.parent.variables)
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                class BusinessRuleTask < ServiceTask
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  def decision_id
         | 
| 116 | 
            +
                    extension_elements&.called_decision&.decision_id
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  def result_variable
         | 
| 120 | 
            +
                    extension_elements&.called_decision&.result_variable
         | 
| 121 | 
            +
                  end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  def run(execution)
         | 
| 124 | 
            +
                    SpotFeel.decide(decision_id, definitions: execution.context.dmn_definitions_by_decision_id(decision_id), variables: execution.parent.variables)
         | 
| 125 | 
            +
                  end
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "bpmn/element"
         | 
| 4 | 
            +
            require_relative "bpmn/extensions"
         | 
| 5 | 
            +
            require_relative "bpmn/extension_elements"
         | 
| 6 | 
            +
            require_relative "bpmn/step"
         | 
| 7 | 
            +
            require_relative "bpmn/flow"
         | 
| 8 | 
            +
            require_relative "bpmn/event_definition"
         | 
| 9 | 
            +
            require_relative "bpmn/event"
         | 
| 10 | 
            +
            require_relative "bpmn/gateway"
         | 
| 11 | 
            +
            require_relative "bpmn/task"
         | 
| 12 | 
            +
            require_relative "bpmn/process"
         | 
| 13 | 
            +
            require_relative "bpmn/definitions"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            module SpotFlow
         | 
| 16 | 
            +
              module Bpmn
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| @@ -0,0 +1,108 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SpotFlow
         | 
| 4 | 
            +
              class Context
         | 
| 5 | 
            +
                attr_reader :sources, :processes, :decisions, :services, :executions
         | 
| 6 | 
            +
                attr_reader :bpmn_definitions, :dmn_definitions
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(sources = [], processes:[], decisions:[], services: {})
         | 
| 9 | 
            +
                  @sources = Array.wrap(sources)
         | 
| 10 | 
            +
                  @processes = Array.wrap(processes)
         | 
| 11 | 
            +
                  @bpmn_definitions = []
         | 
| 12 | 
            +
                  @dmn_definitions = []
         | 
| 13 | 
            +
                  @decisions = Array.wrap(decisions)
         | 
| 14 | 
            +
                  @services = HashWithIndifferentAccess.new((SpotFlow.config.services || {}).merge(services))
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  @sources.each do |source|
         | 
| 17 | 
            +
                    if source.include?("http://www.omg.org/spec/DMN/20180521/DC/")
         | 
| 18 | 
            +
                      definitions = SpotFeel.definitions_from_xml(source)
         | 
| 19 | 
            +
                      @dmn_definitions << definitions
         | 
| 20 | 
            +
                      @decisions += definitions.decisions
         | 
| 21 | 
            +
                    else
         | 
| 22 | 
            +
                      @processes += SpotFlow.processes_from_xml(source)
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  @executions = []
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def start(process_id: nil, start_event_id: nil, variables: {})
         | 
| 30 | 
            +
                  process = process_id ? process_by_id(process_id) : default_process
         | 
| 31 | 
            +
                  raise ExecutionError.new(process_id ? "Process #{process_id} not found." : "No default process found.") if process.blank?
         | 
| 32 | 
            +
                  execution = Execution.start(context: self, process: process, start_event_id: start_event_id, variables: variables)
         | 
| 33 | 
            +
                  executions << execution
         | 
| 34 | 
            +
                  execution
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def start_with_message(message_name:, variables: {})
         | 
| 38 | 
            +
                  [].tap do |executions|
         | 
| 39 | 
            +
                    processes.map do |process|
         | 
| 40 | 
            +
                      process.start_events.map do |start_event|
         | 
| 41 | 
            +
                        start_event.message_event_definitions.map do |message_event_definition|
         | 
| 42 | 
            +
                          if message_name == message_event_definition.message_name
         | 
| 43 | 
            +
                            Execution.start(context: self, process: process, variables: variables, start_event_id: start_event.id).tap { |execution| executions.push execution }
         | 
| 44 | 
            +
                          end
         | 
| 45 | 
            +
                        end
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def restore(execution_state)
         | 
| 52 | 
            +
                  Execution.deserialize(execution_state, context: self).tap do |execution|
         | 
| 53 | 
            +
                    executions << execution
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def notify_listener(*args)
         | 
| 58 | 
            +
                  SpotFlow.config.listener&.call(args)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def default_process
         | 
| 62 | 
            +
                  raise "Multiple processes defined, must identify process" if processes.size > 1
         | 
| 63 | 
            +
                  processes.first
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def process_by_id(id)
         | 
| 67 | 
            +
                  processes.each do |process|
         | 
| 68 | 
            +
                    return process if process.id == id
         | 
| 69 | 
            +
                    process.sub_processes.each do |sub_process|
         | 
| 70 | 
            +
                      return sub_process if sub_process.id == id
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                  nil
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def element_by_id(id)
         | 
| 77 | 
            +
                  processes.each do |process|
         | 
| 78 | 
            +
                    element = process.element_by_id(id)
         | 
| 79 | 
            +
                    return element if element
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
                  nil
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def execution_by_id(id)
         | 
| 85 | 
            +
                  executions.find { |e| e.id == id }
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def execution_by_step_id(step_id)
         | 
| 89 | 
            +
                  executions.find { |e| e.step.id == step_id }
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def decision_by_id(id)
         | 
| 93 | 
            +
                  decisions.find { |d| d.id == id }
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def dmn_definitions_by_decision_id(decision_id)
         | 
| 97 | 
            +
                  dmn_definitions.find { |definitions| definitions.decisions.find { |decision| decision.id == decision_id } }
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def inspect
         | 
| 101 | 
            +
                  parts = ["#<Context"]
         | 
| 102 | 
            +
                  parts << "@processes=#{processes.inspect}" if processes.present?
         | 
| 103 | 
            +
                  parts << "@decisions=#{decisions.inspect}" if decisions.present?
         | 
| 104 | 
            +
                  parts << "@executions=#{executions.inspect}" if executions.present?
         | 
| 105 | 
            +
                  parts.join(" ") + ">"
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
              end
         | 
| 108 | 
            +
            end
         |