camunda-workflow 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|