camunda-workflow 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/CONTRIBUTING.md +20 -0
- data/LICENSE.md +31 -0
- data/README.md +209 -0
- data/lib/camunda.rb +54 -0
- data/lib/camunda/bpmn_xml.rb +52 -0
- data/lib/camunda/deployment.rb +29 -0
- data/lib/camunda/external_task.rb +83 -0
- data/lib/camunda/external_task_job.rb +35 -0
- data/lib/camunda/incident.rb +3 -0
- data/lib/camunda/matchers.rb +20 -0
- data/lib/camunda/model.rb +26 -0
- data/lib/camunda/poller.rb +8 -0
- data/lib/camunda/process_definition.rb +29 -0
- data/lib/camunda/process_instance.rb +21 -0
- data/lib/camunda/signal.rb +9 -0
- data/lib/camunda/task.rb +28 -0
- data/lib/camunda/variable_serialization.rb +48 -0
- data/lib/camunda/workflow.rb +39 -0
- data/lib/generators/camunda/bpmn_classes/USAGE +10 -0
- data/lib/generators/camunda/bpmn_classes/bpmn_classes_generator.rb +67 -0
- data/lib/generators/camunda/bpmn_classes/templates/bpmn_class.rb.template +10 -0
- data/lib/generators/camunda/bpmn_classes/templates/bpmn_module.rb.template +5 -0
- data/lib/generators/camunda/install/USAGE +10 -0
- data/lib/generators/camunda/install/install_generator.rb +11 -0
- data/lib/generators/camunda/install/templates/camunda_job.rb +7 -0
- data/lib/generators/camunda/spring_boot/USAGE +12 -0
- data/lib/generators/camunda/spring_boot/spring_boot_generator.rb +69 -0
- data/lib/generators/camunda/spring_boot/templates/Camunda.java +28 -0
- data/lib/generators/camunda/spring_boot/templates/ProcessScenarioTest.java +85 -0
- data/lib/generators/camunda/spring_boot/templates/application.properties +48 -0
- data/lib/generators/camunda/spring_boot/templates/camunda.cfg.xml +20 -0
- data/lib/generators/camunda/spring_boot/templates/pom.xml +173 -0
- data/lib/generators/camunda/spring_boot/templates/sample.bpmn +94 -0
- metadata +230 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
module Camunda::ExternalTaskJob
|
2
|
+
def perform(id, input_variables)
|
3
|
+
output_variables = bpmn_perform(input_variables)
|
4
|
+
output_variables = {} if output_variables.nil?
|
5
|
+
raise ArgumentError, "Expected a hash, got #{output_variables}" unless output_variables.is_a?(Hash)
|
6
|
+
|
7
|
+
report_completion id, output_variables
|
8
|
+
rescue Camunda::BpmnError => e
|
9
|
+
report_bpmn_error id, e
|
10
|
+
rescue StandardError => e
|
11
|
+
report_failure id, e, input_variables
|
12
|
+
end
|
13
|
+
|
14
|
+
def report_completion(id, variables)
|
15
|
+
# Submit to Camunda using
|
16
|
+
# POST /external-task/{id}/complete
|
17
|
+
Camunda::ExternalTask.new(id: id).complete(variables)
|
18
|
+
end
|
19
|
+
|
20
|
+
def report_failure(id, exception, input_variables)
|
21
|
+
# Submit error state to Camunda using
|
22
|
+
# POST /external-task/{id}/failure
|
23
|
+
Camunda::ExternalTask.new(id: id).failure(exception, input_variables)
|
24
|
+
end
|
25
|
+
|
26
|
+
def report_bpmn_error(id, exception)
|
27
|
+
# Submit bpmn error state to Camunda using
|
28
|
+
# POST /external-task/{id}/bpmnError
|
29
|
+
Camunda::ExternalTask.new(id: id).bpmn_error(exception)
|
30
|
+
end
|
31
|
+
|
32
|
+
def bpmn_perform(_variables)
|
33
|
+
raise StandardError, "Please define this method which takes a hash of variables and returns a hash of variables"
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
RSpec::Matchers.define :have_topics do |topic_names|
|
2
|
+
match { |bpmn_xml| topic_names.sort == bpmn_xml.topics.sort }
|
3
|
+
failure_message { |bpmn_xml| "Expected #{topic_names}. Found #{bpmn_xml.topics.sort}" }
|
4
|
+
end
|
5
|
+
|
6
|
+
RSpec::Matchers.define :have_module do |module_name_expected|
|
7
|
+
match { |bpmn_xml| module_name_expected == bpmn_xml.module_name }
|
8
|
+
failure_message { |bpmn_xml| "ID of the BPMN process is #{bpmn_xml.module_name}. Expected #{module_name_expected}" }
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Matchers.define :have_defined_classes do
|
12
|
+
missing_classes = []
|
13
|
+
match do |bpmn_xml|
|
14
|
+
missing_classes = bpmn_xml.modularized_class_names.reject(&:safe_constantize)
|
15
|
+
missing_classes.empty?
|
16
|
+
end
|
17
|
+
failure_message do |_bpmn_xml|
|
18
|
+
"#{missing_classes} are not defined. They are the expected classes in your Rails app to implement the workers."
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'her/model'
|
2
|
+
class Camunda::Model
|
3
|
+
include Her::Model
|
4
|
+
|
5
|
+
api = lambda do
|
6
|
+
Her::API.new(url: File.join(Camunda::Workflow.configuration.engine_url)) do |c|
|
7
|
+
c.path_prefix = Camunda::Workflow.configuration.engine_route_prefix
|
8
|
+
# Request
|
9
|
+
c.use Faraday::Request::Multipart
|
10
|
+
c.use FaradayMiddleware::EncodeJson
|
11
|
+
# Response
|
12
|
+
c.use Faraday::Response::Logger, ActiveSupport::Logger.new(STDOUT), bodies: true if Rails.env.development?
|
13
|
+
c.use Her::Middleware::FirstLevelParseJSON
|
14
|
+
|
15
|
+
c.use Her::Middleware::SnakeCase
|
16
|
+
# Adapter
|
17
|
+
c.adapter :net_http
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
use_api api
|
22
|
+
|
23
|
+
def self.worker_id
|
24
|
+
Camunda::Workflow.configuration.worker_id
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class Camunda::Poller
|
2
|
+
def self.fetch_and_execute(topics, lock_duration: nil, long_polling_duration: nil)
|
3
|
+
loop do
|
4
|
+
Camunda::ExternalTask
|
5
|
+
.fetch_and_lock(topics, lock_duration: lock_duration, long_polling_duration: long_polling_duration).each(&:queue_task)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Camunda::ProcessDefinition < Camunda::Model
|
2
|
+
include Camunda::VariableSerialization
|
3
|
+
collection_path 'process-definition'
|
4
|
+
|
5
|
+
def self.start_by_key(key, hash={})
|
6
|
+
hash[:variables] = serialize_variables(hash[:variables]) if hash[:variables]
|
7
|
+
tenant_id = hash.delete(:tenant_id)
|
8
|
+
tenant_id ||= Camunda::Workflow.configuration.tenant_id
|
9
|
+
|
10
|
+
response = post_raw start_path_for_key(key, tenant_id), hash
|
11
|
+
raise Camunda::ProcessEngineException, response[:parsed_data][:data][:message] unless response[:response].status == 200
|
12
|
+
|
13
|
+
Camunda::ProcessInstance.new response[:parsed_data][:data]
|
14
|
+
end
|
15
|
+
|
16
|
+
def start(hash={})
|
17
|
+
hash[:variables] = serialize_variables(hash[:variables]) if hash[:variables]
|
18
|
+
response = self.class.post_raw "process-definition/#{id}/start", hash
|
19
|
+
raise Camunda::ProcessEngineException, response[:parsed_data][:data][:message] unless response[:response].status == 200
|
20
|
+
|
21
|
+
Camunda::ProcessInstance.new response[:parsed_data][:data]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.start_path_for_key(key, tenant_id)
|
25
|
+
path = "process-definition/key/#{key}"
|
26
|
+
path << "/tenant-id/#{tenant_id}" if tenant_id
|
27
|
+
"#{path}/start"
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Camunda::ProcessInstance < Camunda::Model
|
2
|
+
collection_path 'process-instance'
|
3
|
+
|
4
|
+
def variables
|
5
|
+
response = self.class.get_raw "process-instance/#{id}/variables"
|
6
|
+
deserialize_variables response[:parsed_data][:data]
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def deserialize_variables(hash)
|
12
|
+
hash.transform_values do |value_hash|
|
13
|
+
case value_hash[:type]
|
14
|
+
when "String", "Double", "Integer", "Boolean"
|
15
|
+
value_hash[:value]
|
16
|
+
when "Json"
|
17
|
+
value_hash[:value][:node_type]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/camunda/task.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
class Camunda::Task < Camunda::Model
|
2
|
+
include Camunda::VariableSerialization
|
3
|
+
collection_path 'task'
|
4
|
+
|
5
|
+
def self.find_by_business_key_and_task_definition_key!(instance_business_key, task_key)
|
6
|
+
find_by(instanceBusinessKey: instance_business_key, taskDefinitionKey: task_key).tap do |ct|
|
7
|
+
unless ct
|
8
|
+
raise MissingTask, "Could not find Camunda Task with processInstanceBusinessKey: #{instance_business_key} " \
|
9
|
+
"and taskDefinitionKey #{task_key}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.mark_task_completed!(instance_business_key, task_key, variables={})
|
15
|
+
ct = find_by_business_key_and_task_definition_key!(instance_business_key, task_key)
|
16
|
+
ct.complete! variables
|
17
|
+
end
|
18
|
+
|
19
|
+
def complete!(vars)
|
20
|
+
self.class.post_raw("#{self.class.collection_path}/#{id}/complete", variables: serialize_variables(vars))[:response]
|
21
|
+
.tap do |response|
|
22
|
+
raise MissingTask unless response.success?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class MissingTask < StandardError
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
module Camunda
|
3
|
+
module VariableSerialization
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def serialize_variables(variables)
|
7
|
+
self.class.serialize_variables(variables)
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
# rubocop:disable Metrics/MethodLength
|
12
|
+
def serialize_variables(variables)
|
13
|
+
hash = variables.transform_values do |value|
|
14
|
+
case value
|
15
|
+
when String
|
16
|
+
{ value: value, type: 'String' }
|
17
|
+
when Array, Hash
|
18
|
+
{ value: transform_json(value).to_json, type: 'Json' }
|
19
|
+
when TrueClass, FalseClass
|
20
|
+
{ value: value, type: 'Boolean' }
|
21
|
+
when Integer
|
22
|
+
{ value: value, type: 'Integer' }
|
23
|
+
when Float
|
24
|
+
{ value: value, type: 'Double' }
|
25
|
+
else
|
26
|
+
raise ArgumentError, "Not supporting complex types yet"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
camelcase_keys(hash)
|
30
|
+
end
|
31
|
+
# rubocop:enable Metrics/MethodLength
|
32
|
+
|
33
|
+
def transform_json(json)
|
34
|
+
if json.is_a?(Array)
|
35
|
+
json.map { |element| transform_json(element) }
|
36
|
+
elsif json.is_a?(Hash)
|
37
|
+
camelcase_keys(json)
|
38
|
+
else
|
39
|
+
json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def camelcase_keys(hash)
|
44
|
+
hash.deep_transform_keys { |key| key.to_s.camelcase(:lower) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Camunda
|
2
|
+
module Workflow
|
3
|
+
def self.configure
|
4
|
+
yield(configuration)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.configuration
|
8
|
+
@configuration ||= Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
class Configuration
|
12
|
+
attr_accessor :engine_url
|
13
|
+
attr_accessor :engine_route_prefix
|
14
|
+
attr_accessor :worker_id
|
15
|
+
attr_accessor :lock_duration
|
16
|
+
attr_accessor :max_polling_tasks
|
17
|
+
attr_accessor :long_polling_duration
|
18
|
+
attr_accessor :tenant_id
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@engine_url = 'http://localhost:8080'
|
22
|
+
@engine_route_prefix = 'rest-engine'
|
23
|
+
@worker_id = '0'
|
24
|
+
@lock_duration = 14.days
|
25
|
+
@max_polling_tasks = 2
|
26
|
+
@long_polling_duration = 30.seconds
|
27
|
+
@tenant_id = if defined?(Rails)
|
28
|
+
Rails.env.test? ? 'test-environment' : nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
%w[../camunda.rb variable_serialization.rb model.rb task.rb external_task.rb external_task_job.rb poller.rb process_definition.rb
|
36
|
+
process_instance.rb deployment.rb signal.rb bpmn_xml.rb incident.rb]
|
37
|
+
.each do |file|
|
38
|
+
require File.join(__dir__, file)
|
39
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
Description:
|
2
|
+
Generates BPMN classes for a BPMN XML in the current Rails app. They will be under app/bpmn. It will first validate the
|
3
|
+
bpmn xml to make sure the class names are all populated as expected.
|
4
|
+
|
5
|
+
Example:
|
6
|
+
rails generate camunda:bpmn_classes bpmn_file
|
7
|
+
|
8
|
+
This will create:
|
9
|
+
app/bpmn/#{process_key}/class1
|
10
|
+
app/bpmn/#{process_key}/class2
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Camunda
|
2
|
+
module Generators
|
3
|
+
class BpmnClassesGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
argument :bpmn_file, type: :string
|
6
|
+
class_option :model_path, type: :string, default: 'app/bpmn'
|
7
|
+
|
8
|
+
def validate_module_name
|
9
|
+
puts "The id of the BPMN process is: #{colored_module_name}. That will be your module name."
|
10
|
+
validate_constant_name(module_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate_class_names
|
14
|
+
bpmn_xml.modularized_class_names.each do |class_name|
|
15
|
+
validate_constant_name(class_name.demodulize, module_name)
|
16
|
+
end
|
17
|
+
puts set_color("External tasks with the same topic name as the BPMN id will be created.", :bold)
|
18
|
+
colorized_class_names = bpmn_xml.modularized_class_names.map! { |class_name| set_color class_name, :red }
|
19
|
+
puts colorized_class_names.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_module
|
23
|
+
template 'bpmn_module.rb.template', File.join(model_path, "#{module_name.underscore}.rb")
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_classes
|
27
|
+
bpmn_xml.class_names_with_same_bpmn_id_as_topic.each do |class_name|
|
28
|
+
template 'bpmn_class.rb.template',
|
29
|
+
File.join(model_path, module_name.underscore, "#{class_name.underscore}.rb"), class_name: class_name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validate_constant_name(name, module_name=nil)
|
36
|
+
top_level = module_name.nil? ? Module.new : module_name.constantize
|
37
|
+
colorized_name = set_color name, :red
|
38
|
+
begin
|
39
|
+
Object.const_set(name, top_level)
|
40
|
+
rescue NameError
|
41
|
+
puts "Cannot create a class/module with name #{colorized_name}. Not a valid name."
|
42
|
+
puts "You must set the ID in Camunda to be the name of a Ruby style constant"
|
43
|
+
end
|
44
|
+
return unless name.include?("_")
|
45
|
+
|
46
|
+
puts "Class name #{colorized_name} should not have an underscore _."
|
47
|
+
puts "Underscores are valid Ruby constant names, but likely you have not changed the name from the default."
|
48
|
+
end
|
49
|
+
|
50
|
+
def model_path
|
51
|
+
options['model_path']
|
52
|
+
end
|
53
|
+
|
54
|
+
def module_name
|
55
|
+
@module_name ||= bpmn_xml.module_name
|
56
|
+
end
|
57
|
+
|
58
|
+
def bpmn_xml
|
59
|
+
@bpmn_xml ||= BpmnXML.new(File.open(bpmn_file))
|
60
|
+
end
|
61
|
+
|
62
|
+
def colored_module_name
|
63
|
+
@colored_module_name ||= set_color module_name, :red
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module <%= module_name %>
|
2
|
+
class <%= config[:class_name] %> < CamundaJob
|
3
|
+
include <%= module_name %>
|
4
|
+
|
5
|
+
def bpmn_perform(_variables)
|
6
|
+
raise StandardError,
|
7
|
+
"Please define this method to perform your task which takes a hash of variables and returns a hash of variables"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
module <%= module_name %>
|
2
|
+
# You can use this module for shared behavior. You may also use ActiveSupport::Concern if you wish.
|
3
|
+
# That means it not only acts as a namespace, but can be be included in a class to let this module behave as a mixin.
|
4
|
+
# extend ActiveSupport::Concern
|
5
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Description:
|
2
|
+
One time install of Java Camunda application to do unit testing of your BPMN process
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate camunda:unit_tests
|
6
|
+
|
7
|
+
This will create a Java app in:
|
8
|
+
bpmn/java_app
|
9
|
+
|
10
|
+
with a sample BPMN file and Unit Test.
|
11
|
+
|
12
|
+
The suggested location for your BPMN file is in `bpmn/diagrams`. `bpmn/java_app/src/main/resources` is a symlink to that.
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Camunda
|
2
|
+
module Generators
|
3
|
+
class SpringBootGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
|
+
class_option :app_path, type: :string, default: 'bpmn/java_app'
|
6
|
+
class_option :diagram_path, type: :string, default: 'bpmn/diagrams'
|
7
|
+
|
8
|
+
def copy_java_app_files
|
9
|
+
copy_file 'pom.xml', File.join(bpmn_app_path, 'pom.xml')
|
10
|
+
copy_file 'camunda.cfg.xml', File.join(bpmn_app_path, 'src/test/resources/camunda.cfg.xml')
|
11
|
+
copy_file 'application.properties', File.join(bpmn_app_path, 'src/main/resources/application.properties')
|
12
|
+
copy_file 'ProcessScenarioTest.java', File.join(bpmn_app_path, 'src/test/java/unittest/ProcessScenarioTest.java')
|
13
|
+
copy_file 'Camunda.java', File.join(bpmn_app_path, 'src/main/java/camunda/Camunda.java')
|
14
|
+
end
|
15
|
+
|
16
|
+
def link_resources_folder
|
17
|
+
copy_file 'sample.bpmn', File.join(diagram_path, 'sample.bpmn'), ''
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_to_ignores
|
21
|
+
%w[.gitignore .cfignore].each do |file|
|
22
|
+
append_to_file file do
|
23
|
+
"\n# BPMN Java app\n" +
|
24
|
+
File.join(bpmn_app_path, 'target') +
|
25
|
+
"\n"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def output_error_instructions
|
31
|
+
puts <<~DOC
|
32
|
+
If you get an error when starting your Rails app
|
33
|
+
|
34
|
+
** ERROR: directory is already being watched! **
|
35
|
+
|
36
|
+
Directory: bpmn/java_app/src/main/resources
|
37
|
+
is already being watched through: bpmn/diagrams
|
38
|
+
|
39
|
+
MORE INFO: https://github.com/guard/listen/wiki/Duplicate-directory-errors
|
40
|
+
|
41
|
+
It is because ActionMailer preview causes test/mailers/previews to get added to the Rails EventedFileChecker
|
42
|
+
by default. RSpec is supposed to override it, but it is not overridden properly for EventedFileChecker and/or
|
43
|
+
you don't have spec/mailers/preview existing. If that directory does not exist it goes to the first common
|
44
|
+
directory that exists which is your Rails root folder.
|
45
|
+
|
46
|
+
So EventedFileChecker is listening to your entire Rails folder. Not a big problem, but it causes a problem
|
47
|
+
for our created symlink.
|
48
|
+
|
49
|
+
So add:
|
50
|
+
|
51
|
+
config.action_mailer.show_previews = false
|
52
|
+
|
53
|
+
to your development.rb file to solve Listen errors about a symlink. Unless you are using ActionMailer
|
54
|
+
previews in which case you should have the directory created already.
|
55
|
+
DOC
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def bpmn_app_path
|
61
|
+
options['app_path']
|
62
|
+
end
|
63
|
+
|
64
|
+
def diagram_path
|
65
|
+
options['diagram_path']
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|