camunda-workflow 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CONTRIBUTING.md +20 -0
  3. data/LICENSE.md +31 -0
  4. data/README.md +209 -0
  5. data/lib/camunda.rb +54 -0
  6. data/lib/camunda/bpmn_xml.rb +52 -0
  7. data/lib/camunda/deployment.rb +29 -0
  8. data/lib/camunda/external_task.rb +83 -0
  9. data/lib/camunda/external_task_job.rb +35 -0
  10. data/lib/camunda/incident.rb +3 -0
  11. data/lib/camunda/matchers.rb +20 -0
  12. data/lib/camunda/model.rb +26 -0
  13. data/lib/camunda/poller.rb +8 -0
  14. data/lib/camunda/process_definition.rb +29 -0
  15. data/lib/camunda/process_instance.rb +21 -0
  16. data/lib/camunda/signal.rb +9 -0
  17. data/lib/camunda/task.rb +28 -0
  18. data/lib/camunda/variable_serialization.rb +48 -0
  19. data/lib/camunda/workflow.rb +39 -0
  20. data/lib/generators/camunda/bpmn_classes/USAGE +10 -0
  21. data/lib/generators/camunda/bpmn_classes/bpmn_classes_generator.rb +67 -0
  22. data/lib/generators/camunda/bpmn_classes/templates/bpmn_class.rb.template +10 -0
  23. data/lib/generators/camunda/bpmn_classes/templates/bpmn_module.rb.template +5 -0
  24. data/lib/generators/camunda/install/USAGE +10 -0
  25. data/lib/generators/camunda/install/install_generator.rb +11 -0
  26. data/lib/generators/camunda/install/templates/camunda_job.rb +7 -0
  27. data/lib/generators/camunda/spring_boot/USAGE +12 -0
  28. data/lib/generators/camunda/spring_boot/spring_boot_generator.rb +69 -0
  29. data/lib/generators/camunda/spring_boot/templates/Camunda.java +28 -0
  30. data/lib/generators/camunda/spring_boot/templates/ProcessScenarioTest.java +85 -0
  31. data/lib/generators/camunda/spring_boot/templates/application.properties +48 -0
  32. data/lib/generators/camunda/spring_boot/templates/camunda.cfg.xml +20 -0
  33. data/lib/generators/camunda/spring_boot/templates/pom.xml +173 -0
  34. data/lib/generators/camunda/spring_boot/templates/sample.bpmn +94 -0
  35. metadata +230 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ef3a0dbf5313cc35e1dbee9a13fb1224dbec5f592bb24449c5e098db593797d6
4
+ data.tar.gz: 1315511b21e6aa52e614e9610bb51e905301ba2bffb9453978cfffb35242bddc
5
+ SHA512:
6
+ metadata.gz: e6392d9dea74d826d93479f34a382e79717cc50f906f0d7f88f246c6d1f79174f6a9a7399898e263d9e8017708517991e752833b99db01f41db6ecb29451bd4f
7
+ data.tar.gz: d08d0468fad93654ddbb940161c0248d88e7e67a496bb6c547a276aa2d24ffe3c6c7e851eca916a1d5af4ebb20244daafd4468f08a4fe6cfaba4dd418e01532d
@@ -0,0 +1,20 @@
1
+ ## Contributing
2
+
3
+ ### Instructions
4
+ Please create an issue to track what you are working on and ensure it is relevant.
5
+
6
+ Make sure whatever you are adding has specs. We should be able to maintain 100% coverage.
7
+
8
+ ### Camunda interactions
9
+
10
+ We are using VCR to record interactions with a local Camunda engine. If you wish to try this run a camunda engine listening on `/rest`. `/rest` is the default of the spring boot application that we generate with this gem. This is different from the default `/rest-engine` of the default downloadable distribution.
11
+
12
+ You can for instance upgrade the Camunda version, delete the `spec/vcr` folder and then re-run the specs with `bundle exec rspec`
13
+
14
+ ## Public domain
15
+
16
+ This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md):
17
+
18
+ > This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/).
19
+ >
20
+ > All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest.
@@ -0,0 +1,31 @@
1
+ As a work of the United States Government, this project is in the
2
+ public domain within the United States.
3
+
4
+ Additionally, we waive copyright and related rights in the work
5
+ worldwide through the CC0 1.0 Universal public domain dedication.
6
+
7
+ ## CC0 1.0 Universal Summary
8
+
9
+ This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
10
+
11
+ ### No Copyright
12
+
13
+ The person who associated a work with this deed has dedicated the work to
14
+ the public domain by waiving all of his or her rights to the work worldwide
15
+ under copyright law, including all related and neighboring rights, to the
16
+ extent allowed by law.
17
+
18
+ You can copy, modify, distribute and perform the work, even for commercial
19
+ purposes, all without asking permission.
20
+
21
+ ### Other Information
22
+
23
+ In no way are the patent or trademark rights of any person affected by CC0,
24
+ nor are the rights that other persons may have in the work or in how the
25
+ work is used, such as publicity or privacy rights.
26
+
27
+ Unless expressly stated otherwise, the person who associated a work with
28
+ this deed makes no warranties about the work, and disclaims liability for
29
+ all uses of the work, to the fullest extent permitted by applicable law.
30
+ When using or citing the work, you should not imply endorsement by the
31
+ author or the affirmer.
@@ -0,0 +1,209 @@
1
+ [![Build Status](https://travis-ci.com/amalagaura/camunda-workflow.svg?branch=master)](https://travis-ci.com/amalagaura/camunda-workflow)
2
+ # Camunda Workflow
3
+
4
+ ## An opinionated interface to Camunda for Ruby/Rails apps
5
+
6
+ [Her](https://github.com/remiprev/her) is used to communicate with the [Camunda REST API](https://docs.camunda.org/manual/latest/reference/rest/).
7
+
8
+ ### Add to your Gemfile
9
+ ```ruby
10
+ gem 'camunda-workflow'
11
+ ```
12
+
13
+ ## Camunda Integration with Ruby
14
+ The process definitions key becomes the module name of your implementation classes and must be set to the name of a ruby style constant (screenshot example provided below). This same process definition key should be set as the topic name for external tasks. Tasks are pulled and fetched and locked and then run. We expect classes (ActiveJob) to implement each external task.
15
+
16
+ ![image](https://www.evernote.com/l/Ajnoawx6CYhKha7OXUPkyeo6CjrxvSoTgOUB/image.png)
17
+
18
+ ### Integration with your worker classes
19
+
20
+ The module `ExternalTaskJob` should be included in your job implementation classes. The job implementation classes can inherit from `ActiveJob::Base`, or use `Sidekiq::Worker` or use some other system for job queuing.
21
+
22
+ Currently we call `perform_later` on job implementation classes. If we want to make this more flexible, we need to make the method used to queue jobs configurable. `perform_later` for ActiveJob, `perform_async` for Sidekiq, or `perform` if no background task system is used.
23
+
24
+ ### Implementing `bpmn_perform`
25
+
26
+ `bpmn_perform` is your implementation of the service task.
27
+
28
+ ### Supporting bpmn exceptions
29
+
30
+ Camunda supports throwing bpmn exceptions on a service task to communicate logic errors and not underlying code errors. These expected errors are thrown with
31
+ ```ruby
32
+ raise Camunda::BpmnError.new error_code: 'bpmn-error', message: "Special BPMN error", variables: { bpmn: 'error' }
33
+ ```
34
+
35
+ ## Generators
36
+
37
+ ### BPMN ActiveJob install
38
+ ```bash
39
+ rails generate camunda:install
40
+ ```
41
+
42
+ Creates `app/jobs/camunda_job.rb`. A class which inherits from ApplicationJob and includes `ExternalTaskJob`. It can be changed to include
43
+ Sidekiq::Worker instead.
44
+
45
+ All of the BPMN worker classes will inherit from this class
46
+
47
+ ### Java Spring Boot App install
48
+ ```bash
49
+ rails generate camunda:spring_boot
50
+ ```
51
+ Creates a skeleton Java Spring Boot app, which also contains the minimal files to run unit tests on a BPMN file. This can be used to
52
+ start a Camunda instance with a REST api. This can also be deployed to PCF by generating a Spring Boot jar and pushing it.
53
+
54
+ ### BPMN Classes
55
+ ```bash
56
+ rails generate camunda:bpmn_classes
57
+ ```
58
+
59
+ Parses the BPMN file and creates task classes according to the ID of the process file and the ID of
60
+ each task. It checks each task and only creates it if the topic name is the same as the process ID. This
61
+ allows one to have some tasks be handled outside the Rails app. It confirms that the ID's are valid Ruby constant names.
62
+
63
+ #### Starting the Camunda server for development
64
+
65
+ Start the application: `mvn spring-boot:run`
66
+
67
+ Camunda-workflow defaults to an in-memory, h2 database engine. If you rather use a Postgres database engine, comment out the
68
+ h2 database engine settings in the `pom.xml` file located in `bpmn/java_app`. Default settings for using Postgres are available in the `pom.xml` file.
69
+ You will need to create a Postgres database on localhost called `camunda`.
70
+
71
+ #### Engine Route Prefix using the Java Spring Boot app
72
+ The default engine route prefix for the provided Java Spring Boot app is `rest`. If you choose to download and use the Camunda distribution,
73
+ the engine prefix is `rest-engine`. Camunda-workflow is configured to use `rest-engine`.
74
+
75
+ To override the default engine route prefix to allow your rails application to use the route prefix of `rest`, you need to add an initializer file
76
+ in your rails app with the below code.
77
+
78
+
79
+ ```ruby
80
+ # filename initializers/camunda.rb
81
+ Camunda::Workflow.configure do |config|
82
+ config.engine_route_prefix = 'rest'
83
+ end
84
+ ```
85
+
86
+ #### Generating a jar for deployment
87
+ `mvn package spring-boot:repackage`
88
+
89
+ The jar is in `target/camunda-bpm-springboot.jar`
90
+
91
+ #### Deploying to PCF
92
+ `cf push app_name -p target/camunda-bpm-springboot.jar`
93
+
94
+ It will fail to start. Create a postgres database as a service in PCF and bind it to the application. The Springboot application is configured for Postgres and will then be able to start.
95
+
96
+ #### Running java unit tests
97
+ `mvn clean test`
98
+
99
+ ## Usage
100
+ ### Deploying a model
101
+ Uses a default name, etc. Below outlines how to deploy a process using the included sample.bpmn
102
+ file created by the generator. Alternatively you can deploy using Camunda Modeler
103
+
104
+ ```ruby
105
+ Camunda::Deployment.create file_name: 'bpmn/diagrams/sample.bpmn'
106
+ ```
107
+ ### Processes
108
+
109
+ #### Starting a process
110
+
111
+ ```ruby
112
+ start_response = Camunda::ProcessDefinition.start_by_key'CamundaWorkflow', variables: { x: 'abcd' }, businessKey: 'WorkflowBusinessKey'
113
+ ```
114
+ **Camunda cannot handle snake case variables, all snake_case variables are serialized to camelCase before a request is sent to the REST api. Variables returned back from the Camunda API will be deserialized back to snake_case.**
115
+
116
+ `{ my_variable: "xyz" }`
117
+
118
+ will be converted to:
119
+
120
+ `{ myVariable: "xyz" }`
121
+
122
+ #### Destroy a process
123
+ ```ruby
124
+ Camunda::ProcessInstance.destroy_existing start_response.id
125
+ ```
126
+
127
+ ### Tasks
128
+
129
+ #### Fetch tasks and queue with ActiveJob
130
+
131
+ The poller will run as an infinite loop with long polling to fetch tasks, queue, and run them. Topic is the process definition key,
132
+ as show in the screenshot example from the Camunda Modeler.
133
+
134
+ Below will run the poller to fetch, lock, and run a task for the example process definition located in
135
+ the `starting a process` detailed above.
136
+
137
+ ```ruby
138
+ Camunda::Poller.fetch_and_execute %w[CamundaWorkflow]
139
+ ```
140
+
141
+ #### Fetch tasks
142
+ For testing from the console
143
+
144
+ ```ruby
145
+ tasks = Camunda::ExternalTask.fetch_and_lock %w[CamundaWorkflow]
146
+ ```
147
+
148
+ #### Run a task
149
+
150
+ ```ruby
151
+ tasks.each(&:run_now)
152
+ ```
153
+
154
+
155
+ ### User Tasks
156
+ #### Mark a user task complete
157
+ ```ruby
158
+ Camunda::Task.mark_task_completed!(business_key, task_key, {})
159
+ ```
160
+
161
+ ### Rspec Helpers
162
+ RSpec helpers can will validate your application to make sure it has a class for every External task in a given BPMN file.
163
+ ```ruby
164
+ require 'camunda/matchers'
165
+
166
+ RSpec.describe "BPMN Diagrams" do
167
+ describe Camunda::BpmnXML.new(File.open("bpmn/diagrams/YourFile.bpmn")) do
168
+ it { is_expected.to have_module('YourModule') }
169
+ it { is_expected.to have_topics(%w[YourModule]) }
170
+ it { is_expected.to have_defined_classes }
171
+ end
172
+ end
173
+ ```
174
+ #### Note:
175
+
176
+ If you get an error
177
+
178
+ ** ERROR: directory is already being watched! **
179
+
180
+ Directory: bpmn/java_app/src/main/resources
181
+ is already being watched through: bpmn/diagrams
182
+
183
+ MORE INFO: https://github.com/guard/listen/wiki/Duplicate-directory-errors
184
+
185
+ It is because ActionMailer preview causes test/mailers/previews to get added to the Rails EventedFileChecker
186
+ by default. RSpec is supposed to override it, but it is not
187
+ appropriately overridden for EventedFileChecker and/or you don't have spec/mailers/preview existing. If that
188
+ directory does not exist, it goes to the first common directory that exists, which is your Rails root folder.
189
+ So EventedFileChecker is listening to your entire Rails folder. Not a big problem, but it causes a problem
190
+ for our created symlink.
191
+
192
+ So add:
193
+
194
+ config.action_mailer.show_previews = false
195
+
196
+ to your `development.rb` file to solve listen errors about a symlink. Unless you are using ActionMailer
197
+ previews, in which case you should have the directory created already.
198
+
199
+ ## Contributing
200
+
201
+ See [CONTRIBUTING](CONTRIBUTING.md) for additional information.
202
+
203
+ ## Public domain
204
+
205
+ This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md):
206
+
207
+ > This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/).
208
+ >
209
+ > All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest.
@@ -0,0 +1,54 @@
1
+ require 'active_support/core_ext/string/inflections.rb'
2
+ require 'active_support/core_ext/object/blank.rb'
3
+ require 'her'
4
+ require 'faraday'
5
+ require 'faraday_middleware'
6
+
7
+ module Camunda
8
+ class << self
9
+ attr_writer :logger
10
+
11
+ def logger
12
+ @logger ||= Logger.new($stdout).tap do |log|
13
+ log.progname = name
14
+ end
15
+ end
16
+ end
17
+
18
+ class Her::Middleware::SnakeCase < Faraday::Response::Middleware
19
+ def on_complete(env)
20
+ return if env[:body].blank?
21
+
22
+ json = JSON.parse(env[:body])
23
+ if json.is_a?(Array)
24
+ json.map { |hash| transform_hash!(hash) }
25
+ elsif json.is_a?(Hash)
26
+ transform_hash!(json)
27
+ end
28
+ env[:body] = JSON.generate(json)
29
+ end
30
+
31
+ def transform_hash!(hash)
32
+ hash.deep_transform_keys!(&:underscore)
33
+ end
34
+ end
35
+
36
+ class MissingImplementationClass < StandardError
37
+ def initialize(class_name)
38
+ super "Class to run a Camunda activity does not exist. Ensure there is a class with name: #{class_name} available."
39
+ end
40
+ end
41
+
42
+ class ProcessEngineException < StandardError
43
+ end
44
+
45
+ class BpmnError < StandardError
46
+ attr_reader :error_code, :variables
47
+
48
+ def initialize(message:, error_code:, variables: {})
49
+ super(message)
50
+ @error_code = error_code
51
+ @variables = variables
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ class Camunda::BpmnXML
2
+ attr_reader :doc
3
+ def initialize(io_or_string)
4
+ @doc = Nokogiri::XML(io_or_string)
5
+ end
6
+
7
+ def to_s
8
+ module_name
9
+ end
10
+
11
+ def module_name
12
+ @doc.xpath('/bpmn:definitions/bpmn:process').first['id']
13
+ end
14
+
15
+ def external_tasks
16
+ @doc.xpath('//*[@camunda:type="external"]').map do |task|
17
+ Task.new(task)
18
+ end
19
+ end
20
+
21
+ def class_names_with_same_bpmn_id_as_topic
22
+ tasks_with_same_bpmn_id_as_topic.map(&:class_name)
23
+ end
24
+
25
+ def modularized_class_names
26
+ class_names_with_same_bpmn_id_as_topic.map { |name| "#{module_name}::#{name}" }
27
+ end
28
+
29
+ def topics
30
+ @doc.xpath('//*[@camunda:topic]').map { |node| node.attribute('topic').value }.uniq
31
+ end
32
+
33
+ private
34
+
35
+ def tasks_with_same_bpmn_id_as_topic
36
+ external_tasks.select { |task| task.topic == module_name }
37
+ end
38
+
39
+ class Task
40
+ def initialize(task)
41
+ @task = task
42
+ end
43
+
44
+ def class_name
45
+ @task.attribute('id').value
46
+ end
47
+
48
+ def topic
49
+ @task.attribute('topic').value
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ class Camunda::Deployment < Camunda::Model
2
+ collection_path 'deployment'
3
+ # Only supporting .create which uses a POST on deployment/create.
4
+ def self.create(file_names:, tenant_id: nil, deployment_source: 'Camunda Workflow Gem', deployment_name: nil)
5
+ deployment_name ||= file_names.map { |file_name| File.basename(file_name) }.join(", ")
6
+ tenant_id ||= Camunda::Workflow.configuration.tenant_id
7
+ args = file_data(file_names).merge('deployment-name' => deployment_name, 'deployment-source' => deployment_source)
8
+ args.merge!("tenant-id": tenant_id) if tenant_id
9
+ response = post_raw('deployment/create', args)
10
+
11
+ deployed_process_definitions(response[:parsed_data][:data][:deployed_process_definitions])
12
+ end
13
+
14
+ def self.file_data(file_names)
15
+ file_names.map do |file_name|
16
+ [file_name, UploadIO.new(file_name, 'text/plain')]
17
+ end.to_h
18
+ end
19
+
20
+ def self.deployed_process_definitions(definitions_hash)
21
+ # Currently only returning the process definitions. But this Deployment.create can create a DMN, CMMN also
22
+ # It returns :deployed_process_definitions, :deployed_case_definitions, :deployed_decision_definitions,
23
+ # :deployed_decision_requirements_definitions
24
+
25
+ raise Camunda::ProcessEngineException, "No Process Definition created" if definitions_hash.nil?
26
+
27
+ definitions_hash.values.map { |process_definition| Camunda::ProcessDefinition.new process_definition }
28
+ end
29
+ end
@@ -0,0 +1,83 @@
1
+ require 'active_support/core_ext/string/inflections.rb'
2
+
3
+ class Camunda::ExternalTask < Camunda::Model
4
+ include Camunda::VariableSerialization
5
+ collection_path 'external-task'
6
+ custom_post :fetchAndLock, :unlock
7
+
8
+ def self.long_polling_duration
9
+ Camunda::Workflow.configuration.long_polling_duration.in_milliseconds
10
+ end
11
+
12
+ def self.max_polling_tasks
13
+ Camunda::Workflow.configuration.max_polling_tasks
14
+ end
15
+
16
+ def self.lock_duration
17
+ Camunda::Workflow.configuration.lock_duration.in_milliseconds
18
+ end
19
+
20
+ def failure(exception, input_variables)
21
+ variables_information = "Input variables are #{input_variables.inspect}\n\n"
22
+ self.class.post_raw("#{collection_path}/#{id}/failure",
23
+ workerId: worker_id, errorMessage: exception.message,
24
+ errorDetails: variables_information + exception.full_message)[:response]
25
+ end
26
+
27
+ def bpmn_error(bpmn_exception)
28
+ self.class.post_raw("#{collection_path}/#{id}/bpmnError",
29
+ workerId: worker_id, variables: serialize_variables(bpmn_exception.variables),
30
+ errorCode: bpmn_exception.error_code, errorMessage: bpmn_exception.message)[:response]
31
+ end
32
+
33
+ def complete(variables={})
34
+ self.class.post_raw("#{collection_path}/#{id}/complete",
35
+ workerId: worker_id, variables: serialize_variables(variables))[:response]
36
+ end
37
+
38
+ def worker_id
39
+ self.class.worker_id
40
+ end
41
+
42
+ def collection_path
43
+ self.class.collection_path
44
+ end
45
+
46
+ def variables
47
+ super.transform_values do |details|
48
+ if details['type'] == 'Json'
49
+ JSON.parse(details['value'])
50
+ else
51
+ details['value']
52
+ end
53
+ end
54
+ end
55
+
56
+ def queue_task
57
+ task_class.perform_later(id, variables)
58
+ end
59
+
60
+ def run_now
61
+ task_class_name.safe_constantize.perform_now id, variables
62
+ end
63
+
64
+ def self.fetch_and_lock(topics, lock_duration: nil, long_polling_duration: nil)
65
+ long_polling_duration ||= long_polling_duration()
66
+ lock_duration ||= lock_duration()
67
+ topic_details = Array(topics).map do |topic|
68
+ { topicName: topic, lockDuration: lock_duration }
69
+ end
70
+ fetchAndLock workerId: worker_id, maxTasks: max_polling_tasks, asyncResponseTimeout: long_polling_duration,
71
+ topics: topic_details
72
+ end
73
+
74
+ def task_class_name
75
+ "#{process_definition_key}::#{activity_id}"
76
+ end
77
+
78
+ def task_class
79
+ task_class_name.safe_constantize.tap do |klass|
80
+ raise Camunda::MissingImplementationClass, task_class_name unless klass
81
+ end
82
+ end
83
+ end