flow_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +255 -0
  4. data/Rakefile +29 -0
  5. data/app/models/flow_core/application_record.rb +7 -0
  6. data/app/models/flow_core/arc.rb +43 -0
  7. data/app/models/flow_core/arc_guard.rb +18 -0
  8. data/app/models/flow_core/end_place.rb +17 -0
  9. data/app/models/flow_core/instance.rb +115 -0
  10. data/app/models/flow_core/place.rb +63 -0
  11. data/app/models/flow_core/start_place.rb +17 -0
  12. data/app/models/flow_core/task.rb +197 -0
  13. data/app/models/flow_core/token.rb +105 -0
  14. data/app/models/flow_core/transition.rb +143 -0
  15. data/app/models/flow_core/transition_callback.rb +24 -0
  16. data/app/models/flow_core/transition_trigger.rb +24 -0
  17. data/app/models/flow_core/workflow.rb +131 -0
  18. data/config/routes.rb +4 -0
  19. data/db/migrate/20200130200532_create_flow_core_tables.rb +151 -0
  20. data/lib/flow_core.rb +25 -0
  21. data/lib/flow_core/arc_guardable.rb +15 -0
  22. data/lib/flow_core/definition.rb +17 -0
  23. data/lib/flow_core/definition/callback.rb +33 -0
  24. data/lib/flow_core/definition/guard.rb +33 -0
  25. data/lib/flow_core/definition/net.rb +111 -0
  26. data/lib/flow_core/definition/place.rb +33 -0
  27. data/lib/flow_core/definition/transition.rb +107 -0
  28. data/lib/flow_core/definition/trigger.rb +33 -0
  29. data/lib/flow_core/engine.rb +6 -0
  30. data/lib/flow_core/errors.rb +14 -0
  31. data/lib/flow_core/locale/en.yml +26 -0
  32. data/lib/flow_core/task_executable.rb +34 -0
  33. data/lib/flow_core/transition_callbackable.rb +33 -0
  34. data/lib/flow_core/transition_triggerable.rb +27 -0
  35. data/lib/flow_core/version.rb +5 -0
  36. data/lib/flow_core/violations.rb +253 -0
  37. data/lib/tasks/flow_core_tasks.rake +6 -0
  38. metadata +123 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8930957495dfe27e025aaebc7b0c88b665cd421ddcba835cc4680776279553d
4
+ data.tar.gz: 02d2f3166cde5e4d90226163d05ba8ffac13a26df81e1221040fbf8914933b5c
5
+ SHA512:
6
+ metadata.gz: 96ce06af9cce87d138115bce55601dd0d3e278192c6052b82546ed1e2fad741876c5f0f01fbe71d37d01b04c60bf8d46e2a3c289f616d25bd5316cdff65a646a
7
+ data.tar.gz: 6fbc984298817220c0569484d40e7d8136013ad499d62d119fe43bfb6621a00a2056c59c07d1bf6bae6dfa82e91c06c1775a82a0d2f1e87ae093e540f43ec8fa
@@ -0,0 +1,20 @@
1
+ Copyright 2020 jasl
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,255 @@
1
+ Flow Core
2
+ ===
3
+
4
+ > FlowCore is ready for open reviewing, but it haven't tested in production yet,
5
+ > any help are most welcome, breaking change is acceptable.
6
+
7
+ A multi purpose, extendable, Workflow-net-based workflow engine for Rails applications.
8
+
9
+ FlowCore is an open source Rails engine provides core workflow functionalities,
10
+ including workflow definition and workflow instance scheduling.
11
+ Easily making automation (including CI, CD, Data processing, etc.) and BPM applications or help you solve parts which changing frequently.
12
+
13
+ ## Features
14
+
15
+ ### Support all databases which based on ActiveRecord
16
+
17
+ All persistent data are present as ActiveRecord model and not use any DB-specific feature.
18
+
19
+ ### Easy to extend & hack
20
+
21
+ FlowCore basically followed best practice of Rails engine,
22
+ you can extend as [Rails Guides](https://guides.rubyonrails.org/engines.html#improving-engine-functionality) suggests.
23
+
24
+ Your app-specific workflow triggers, callbacks and guards can be extended via [Single Table Inheritance](https://guides.rubyonrails.org/association_basics.html#single-table-inheritance)
25
+
26
+ FlowCore also provides callbacks for triggers (which control behavior of a transition) covered whole task lifecycle.
27
+
28
+ ### Petri-net based
29
+
30
+ [Petri-net](https://en.wikipedia.org/wiki/Petri_net) is a technique for description and analysis of concurrent systems.
31
+
32
+ FlowCore choose its special type called [Workflow-net](http://mlwiki.org/index.php/Workflow_Nets) to expressing workflow.
33
+
34
+ Compared to more popular activity-based workflow definitions (e.g BPMN),
35
+ Petri-net has only few rules but could express very complex case.
36
+
37
+ Check [workflow patterns](http://workflowpatterns.com) to learn how to use Petri-net expressing workflows.
38
+
39
+ ### Basic workflow checking.
40
+
41
+ A workflow should be verified first before running it.
42
+
43
+ FlowCore provides the mechanism to help to prevent unexpected error on instance running
44
+
45
+ > This is the hard work and help wanted
46
+ > Workflow-net has [soundness](http://mlwiki.org/index.php/Workflow_Soundness) checking but I don't know how to implement it
47
+
48
+ ### Interfaces and abstractions to integrate your business
49
+
50
+ FlowCore separate app-world and engine-world using interfaces and abstract classes,
51
+ basically you no need to know Workflow-net internal works.
52
+
53
+ ### Runtime error and suspend support
54
+
55
+ FlowCore provides necessary columns and event callbacks for runtime error and suspend.
56
+
57
+ ### A DSL to simplify workflow creation
58
+
59
+ FlowCore provides a powerful DSL for creating workflow.
60
+
61
+ ## Demo
62
+
63
+ **You need to install Graphviz first**
64
+
65
+ Clone the repository.
66
+
67
+ ```sh
68
+ $ git clone https://github.com/rails-engine/flow_core.git
69
+ ```
70
+
71
+ Change directory
72
+
73
+ ```sh
74
+ $ cd flow_core
75
+ ```
76
+
77
+ Run bundler
78
+
79
+ ```sh
80
+ $ bundle install
81
+ ```
82
+
83
+ Preparing database
84
+
85
+ ```sh
86
+ $ bin/rails db:migrate
87
+ ```
88
+
89
+ Import sample workflow
90
+
91
+ ```sh
92
+ $ bin/rails db:seed
93
+ ```
94
+
95
+ Start the Rails server
96
+
97
+ ```sh
98
+ $ bin/rails s
99
+ ```
100
+
101
+ Open your browser, and visit `http://localhost:3000`
102
+
103
+ ## Design
104
+
105
+ Architecture:
106
+
107
+ ![Architecture](doc/assets/architecture.png)
108
+
109
+ Basic design based on [An activity based Workflow Engine for PHP By Tony Marston](https://www.tonymarston.net/php-mysql/workflow.html).
110
+
111
+ Some notable:
112
+
113
+ - Arc: The line to connecting a Place and a Transition
114
+ - ArcGuard: The matcher to decide the arc is passable,
115
+ it's an base class that you can extend it for your own purpose.
116
+ - Task: A stateful record to present current workflow instance work,
117
+ and can reference a `TaskExecutable` through Rails polymorphic-reference.
118
+ It finish means the transition is done and can moving on.
119
+ - TaskExecutable: An interface for binding App task and FlowCore Task.
120
+ - TransitionTrigger: It controls the behavior of a Transition,
121
+ it's an base class that you can extend it for your own purpose,
122
+ best place for implementing business.
123
+ - TransitionCallback: It can be registered to a Transition, and be triggered on specified lifecycle(s) of Task
124
+
125
+ ### Lifecycle of Task
126
+
127
+ - `created` Task created by a Petri-net Token
128
+ - `enabled` Transit to this stage when next transition requirement fulfilled
129
+ - Best chance to create app task (your custom task for business) in `TransitionTrigger#on_task_enabled`
130
+ - `finished` Normal ending
131
+ - Require app task finished first (if bind)
132
+ - `terminated` Task killed by instance (e.g Instance cancelled) or other race condition task
133
+
134
+ ### FlowKit
135
+
136
+ Because FlowCore only care about essentials of workflow engine,
137
+ I'm planning a gem based on FlowCore to provides BPM-oriented features, including:
138
+
139
+ - Dynamic form
140
+ - Approval Task with assignment
141
+ - ExpressionGuard
142
+
143
+ ### Why "core"
144
+
145
+ Because it's not aim to "out-of-box",
146
+ some gem like Devise giving developer an out-of-box experience, that's awesome,
147
+ but on the other hand, it also introducing a very complex abstraction that may hard to understanding how it works,
148
+ especially when you attempting to customize it.
149
+
150
+ I believe that the gem is tightly coupled with features that face to end users directly,
151
+ so having a good customizability and easy to understanding are of the most concern,
152
+ so I just wanna give you a domain framework that you can build your own that just fitting your need,
153
+ and you shall have fully control and without any unnecessary abstraction.
154
+
155
+ ## TODO / Help wanted
156
+
157
+ - Document
158
+ - Test
159
+ - Activity-based to Petri-net mapping, see <https://www.researchgate.net/figure/The-mapping-between-BPMN-and-Petri-nets_tbl2_221250389> for example.
160
+ - More efficient and powerful workflow definition checking
161
+ - Grammar and naming correction (I'm not English native-speaker)
162
+
163
+ ## Usage
164
+
165
+ > WIP
166
+
167
+ ### Deploy a workflow
168
+
169
+ See [test/dummy/db/seeds.rb](test/dummy/db/seeds.rb) to learn the DSL, more complex sample see [test/dummy/app/models/internal_workflow.rb](test/dummy/app/models/internal_workflow.rb)
170
+
171
+ ### Running a workflow
172
+
173
+ `workflow.create_instance!`
174
+
175
+ ### Implementing an ArcGuard
176
+
177
+ [test/dummy/app/models/arc_guards/dentaku.rb](test/dummy/app/models/arc_guards/dentaku.rb) shows an expression guard which using [Dentaku](https://github.com/rubysolo/dentaku)
178
+
179
+ ### Implementing a TransitionTrigger
180
+
181
+ [test/dummy/app/models/transition_triggers/timer.rb](test/dummy/app/models/transition_triggers/timer.rb) shows a delayed trigger which can be used for expires.
182
+
183
+ [test/dummy/app/models/transition_triggers/user_task.rb](test/dummy/app/models/transition_triggers/user_task.rb) shows a simple user task with a simple assignment.
184
+
185
+ ### Implementing a TransitionCallback
186
+
187
+ [test/dummy/app/models/transition_callbacks/notification.rb](test/dummy/app/models/transition_callbacks/notification.rb) shows a simple callback that notify the assignee when the task started
188
+
189
+ ### Implementing a TaskExecutable
190
+
191
+ [test/dummy/app/models/user_task.rb](test/dummy/app/models/user_task.rb) shows a sample,
192
+ [test/dummy/app/models/approval_task.rb](test/dummy/app/models/approval_task.rb) shows how to set payload to task that use for ArcGuard
193
+
194
+ ### Extending Workflow
195
+
196
+ [test/dummy/app/models/internal_workflow.rb](test/dummy/app/models/internal_workflow.rb) shows how to use STI extending Workflow.
197
+
198
+ [test/dummy/app/overrides/models/flow_core/workflow_override.rb](test/dummy/app/overrides/models/flow_core/workflow_override.rb) shows how to apply Rails override pattern to extend base model,
199
+ here I add `to_graphviz` for dummy app visualize workflows.
200
+
201
+ ## Requirement
202
+
203
+ - Rails 6.0+
204
+ - Ruby 2.5+
205
+
206
+ ## Installation
207
+
208
+ Add this line to your application's Gemfile:
209
+
210
+ ```ruby
211
+ gem "flow_core"
212
+ ```
213
+
214
+ Or you may want to include the gem directly from GitHub:
215
+
216
+ ```ruby
217
+ gem 'flow_core', github: 'rails-engine/flow_core'
218
+ ```
219
+
220
+ And then execute:
221
+
222
+ ```bash
223
+ $ bundle
224
+ ```
225
+
226
+ Or install it yourself as:
227
+
228
+ ```bash
229
+ $ gem install flow_core
230
+ ```
231
+
232
+ ## References
233
+
234
+ - [hooopo/petri_flow](https://github.com/hooopo/petri_flow) (my partner's version, we share the same basis)
235
+ - <http://mlwiki.org/index.php/Petri_Nets>
236
+ - <https://www.tonymarston.net/php-mysql/workflow.html>
237
+ - <http://workflowpatterns.com/>
238
+
239
+ ## Contributing
240
+
241
+ Bug report or pull request are welcome.
242
+
243
+ ### Make a pull request
244
+
245
+ 1. Fork it
246
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
247
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
248
+ 4. Push to the branch (`git push origin my-new-feature`)
249
+ 5. Create new Pull Request
250
+
251
+ Please write unit test with your code if necessary.
252
+
253
+ ## License
254
+
255
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "rdoc/task"
5
+
6
+ RDoc::Task.new(:rdoc) do |rdoc|
7
+ rdoc.rdoc_dir = "rdoc"
8
+ rdoc.title = "FlowCore"
9
+ rdoc.options << "--line-numbers"
10
+ rdoc.rdoc_files.include("README.md")
11
+ rdoc.rdoc_files.include("lib/**/*.rb")
12
+ end
13
+
14
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
15
+ load "rails/tasks/engine.rake"
16
+
17
+ load "rails/tasks/statistics.rake"
18
+
19
+ require "bundler/gem_tasks"
20
+
21
+ require "rake/testtask"
22
+
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.libs << "test"
25
+ t.pattern = "test/**/*_test.rb"
26
+ t.verbose = false
27
+ end
28
+
29
+ task default: :test
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class Arc < FlowCore::ApplicationRecord
5
+ self.table_name = "flow_core_arcs"
6
+
7
+ belongs_to :workflow, class_name: "FlowCore::Workflow"
8
+ belongs_to :transition, class_name: "FlowCore::Transition"
9
+ belongs_to :place, class_name: "FlowCore::Place"
10
+
11
+ has_many :guards, class_name: "FlowCore::ArcGuard", dependent: :delete_all
12
+
13
+ enum direction: {
14
+ in: 0,
15
+ out: 1
16
+ }
17
+
18
+ validates :place,
19
+ uniqueness: {
20
+ scope: %i[workflow transition direction]
21
+ }
22
+
23
+ before_destroy :prevent_destroy
24
+ after_create :reset_workflow_verification
25
+ after_destroy :reset_workflow_verification
26
+
27
+ def can_destroy?
28
+ workflow.instances.empty?
29
+ end
30
+
31
+ private
32
+
33
+ def reset_workflow_verification
34
+ workflow.reset_workflow_verification!
35
+ end
36
+
37
+ def prevent_destroy
38
+ unless can_destroy?
39
+ raise FlowCore::ForbiddenOperation, "Found exists instance, destroy transition will lead serious corruption"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class ArcGuard < FlowCore::ApplicationRecord
5
+ self.table_name = "flow_core_arc_guards"
6
+
7
+ belongs_to :workflow, class_name: "FlowCore::Workflow"
8
+ belongs_to :arc, class_name: "FlowCore::Arc"
9
+
10
+ has_one :transition, through: :arc, class_name: "FlowCore::Transition"
11
+
12
+ before_validation do
13
+ self.workflow ||= arc&.workflow
14
+ end
15
+
16
+ include FlowCore::ArcGuardable
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class EndPlace < FlowCore::Place
5
+ validates :type,
6
+ uniqueness: {
7
+ scope: :workflow
8
+ }
9
+
10
+ validates :output_arcs,
11
+ length: { is: 0 }
12
+
13
+ def end?
14
+ true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class Instance < FlowCore::ApplicationRecord
5
+ self.table_name = "flow_core_instances"
6
+
7
+ FORBIDDEN_ATTRIBUTES = %i[
8
+ workflow_id stage activated_at finished_at canceled_at terminated_at terminated_reason
9
+ errored_at rescued_at suspended_at resumed_at created_at updated_at
10
+ ].freeze
11
+
12
+ belongs_to :workflow, class_name: "FlowCore::Workflow"
13
+
14
+ has_many :tokens, class_name: "FlowCore::Token", dependent: :delete_all
15
+ has_many :tasks, class_name: "FlowCore::Task", dependent: :delete_all
16
+
17
+ serialize :payload
18
+
19
+ enum stage: {
20
+ created: 0,
21
+ activated: 1,
22
+ canceled: 2,
23
+ finished: 11,
24
+ terminated: 12
25
+ }
26
+
27
+ scope :errored, -> { where.not(errored_at: nil) }
28
+ scope :suspended, -> { where.not(suspended_at: nil) }
29
+
30
+ after_initialize do
31
+ self.payload ||= {}
32
+ end
33
+
34
+ def errored?
35
+ errored_at.present?
36
+ end
37
+
38
+ def suspended?
39
+ suspended_at.present?
40
+ end
41
+
42
+ def can_active?
43
+ created?
44
+ end
45
+
46
+ def can_finish?
47
+ activated?
48
+ end
49
+
50
+ def active
51
+ return false unless can_active?
52
+
53
+ transaction do
54
+ tokens.create! place: workflow.start_place
55
+ update! stage: :activated, activated_at: Time.zone.now
56
+ end
57
+
58
+ true
59
+ end
60
+
61
+ def finish
62
+ return false unless can_finish?
63
+
64
+ transaction do
65
+ update! stage: :finished, finished_at: Time.zone.now
66
+
67
+ tasks.where(stage: %i[created enabled]).find_each do |task|
68
+ task.terminate! reason: "Instance finished"
69
+ end
70
+ tokens.where(stage: %i[free locked]).find_each(&:terminate!)
71
+ end
72
+
73
+ true
74
+ end
75
+
76
+ def active!
77
+ active || raise(FlowCore::InvalidTransition, "Can't active Instance##{id}")
78
+ end
79
+
80
+ def finish!
81
+ finish || raise(FlowCore::InvalidTransition, "Can't finish Instance##{id}")
82
+ end
83
+
84
+ def error!
85
+ return if errored?
86
+
87
+ update! errored_at: Time.zone.now
88
+ end
89
+
90
+ def rescue!
91
+ return unless errored?
92
+ return unless tasks.errored.any?
93
+
94
+ update! errored_at: nil, rescued_at: Time.zone.now
95
+ end
96
+
97
+ def suspend!
98
+ return if suspended?
99
+
100
+ transaction do
101
+ tasks.enabled.each(&:suspend!)
102
+ update! suspended_at: Time.zone.now
103
+ end
104
+ end
105
+
106
+ def resume!
107
+ return unless suspended?
108
+
109
+ transaction do
110
+ tasks.enabled.each(&:resume!)
111
+ update! suspended_at: nil, resumed_at: Time.zone.now
112
+ end
113
+ end
114
+ end
115
+ end