flow_core 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d31b83d470547f95bbe973b6376c596a2d2ebe7c5d15efe569452a4342cfe5b8
4
- data.tar.gz: 1b7ae085f8487d2d6765cb946435b5f83c1d32bbea26e625303ee8941ec2efa1
3
+ metadata.gz: 743fbfab50000cf6b47c17fe0d90d962faad47fb8eeb0cb44df1e777b56fff0d
4
+ data.tar.gz: 67d9e0a9f7e7f47d0d958116f4e10d5e885e80ce6b929aa49c49ae844cdc2c18
5
5
  SHA512:
6
- metadata.gz: 1765159897d571c89c05b3cca8f3d30229a13e6511a3797f8bfda0620540ac97940132bcc019a80fd371b1db8680bba91cca27927c841a54e0c4f48f88ad80ca
7
- data.tar.gz: c6d354ddecbdda0596132ad83d56fce39bcbb9ab05a7a642e4c20bfa557ef792a0932156d6be195d8a66ff7e6143fc076a46a166e62be6cc77e33152a888d1de
6
+ metadata.gz: e7b941b64b50e8d083832c3faf1c22f729473fbf1ac7ed1a0ba67f0831bb9d1a7078f95c008f18ae77138358be376124c47c094fb97d6b41ecf18e99f78a0c68
7
+ data.tar.gz: 35f944cd17e0b9d07afc0903198ffa9cc43f7d74858fa778112ae4a7c69a0de6a59e21354ab9c4460e2eafb8800f790aa12270dce06ffc85e1bfcafd643e5fca
data/README.md CHANGED
@@ -92,6 +92,13 @@ Import sample workflow
92
92
  $ bin/rails db:seed
93
93
  ```
94
94
 
95
+ Build Script Core
96
+
97
+ ```sh
98
+ $ bin/rails app:script_core:engine:build
99
+ $ bin/rails app:script_core:engine:compile_lib
100
+ ```
101
+
95
102
  Start the Rails server
96
103
 
97
104
  ```sh
@@ -19,6 +19,14 @@ module FlowCore
19
19
  uniqueness: {
20
20
  scope: %i[workflow transition direction]
21
21
  }
22
+ validates :fallback_arc,
23
+ uniqueness: {
24
+ scope: %i[workflow transition direction]
25
+ }, if: :fallback_arc?
26
+
27
+ before_validation on: :create do
28
+ self.workflow ||= place&.workflow || transition&.workflow
29
+ end
22
30
 
23
31
  before_destroy :prevent_destroy
24
32
  after_create :reset_workflow_verification
@@ -4,13 +4,38 @@ module FlowCore
4
4
  class ArcGuard < FlowCore::ApplicationRecord
5
5
  self.table_name = "flow_core_arc_guards"
6
6
 
7
- belongs_to :workflow, class_name: "FlowCore::Workflow"
8
- belongs_to :arc, class_name: "FlowCore::Arc"
7
+ belongs_to :workflow, class_name: "FlowCore::Workflow", optional: true
8
+ belongs_to :arc, class_name: "FlowCore::Arc", optional: true
9
9
 
10
10
  has_one :transition, through: :arc, class_name: "FlowCore::Transition"
11
11
 
12
+ belongs_to :pipeline, class_name: "FlowCore::Pipeline", optional: true
13
+ belongs_to :branch, class_name: "FlowCore::Branch", optional: true
14
+
15
+ validates :arc,
16
+ presence: true,
17
+ if: ->(r) { r.workflow }
18
+ validates :branch,
19
+ presence: true,
20
+ if: ->(r) { r.pipeline }
21
+ validates :workflow,
22
+ presence: true,
23
+ if: ->(r) { !r.pipeline }
24
+ validates :pipeline,
25
+ presence: true,
26
+ if: ->(r) { !r.workflow }
27
+
12
28
  before_validation do
13
29
  self.workflow ||= arc&.workflow
30
+ self.pipeline ||= branch&.pipeline
31
+ end
32
+
33
+ def configurable?
34
+ false
35
+ end
36
+
37
+ def type_key
38
+ self.class.to_s.split("::").last.underscore
14
39
  end
15
40
 
16
41
  include FlowCore::ArcGuardable
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class Branch < FlowCore::ApplicationRecord
5
+ self.table_name = "flow_core_branches"
6
+
7
+ belongs_to :pipeline, class_name: "FlowCore::Pipeline"
8
+ belongs_to :step, class_name: "FlowCore::Step"
9
+
10
+ has_many :arc_guards, class_name: "FlowCore::ArcGuard", dependent: :destroy
11
+
12
+ has_many :steps, -> { order(position: :asc) },
13
+ class_name: "FlowCore::Step", inverse_of: :branch,
14
+ dependent: :destroy
15
+
16
+ validates :name,
17
+ presence: true
18
+
19
+ validates :fallback_branch,
20
+ uniqueness: {
21
+ scope: %i[step_id]
22
+ }, if: :fallback_branch?
23
+
24
+ before_validation do
25
+ self.pipeline ||= step.pipeline
26
+ end
27
+
28
+ def user_destroyable?
29
+ !fallback_branch || (fallback_branch? && !step.fallback_branch_required?)
30
+ end
31
+
32
+ def arc_guard_attachable?
33
+ !fallback_branch? && step.branch_arc_guard_attachable?
34
+ end
35
+
36
+ def copy_arc_guards_to(arc)
37
+ return unless arc_guard_attachable?
38
+
39
+ arc_guards.find_each do |arc_guard|
40
+ new_guard = arc_guard.dup
41
+ new_guard.arc = arc
42
+ new_guard.pipeline = nil
43
+ new_guard.branch = nil
44
+ new_guard.save!
45
+ end
46
+ end
47
+ end
48
+ end
@@ -27,6 +27,8 @@ module FlowCore
27
27
  scope :errored, -> { where.not(errored_at: nil) }
28
28
  scope :suspended, -> { where.not(suspended_at: nil) }
29
29
 
30
+ validate :on_create_validation, on: :create
31
+
30
32
  after_initialize do
31
33
  self.payload ||= {}
32
34
  end
@@ -66,10 +68,11 @@ module FlowCore
66
68
  return false unless can_activate?
67
69
 
68
70
  with_transaction_returning_status do
69
- tokens.create! place: workflow.start_place
70
71
  update! stage: :activated, activated_at: Time.zone.now
71
72
  workflow.on_instance_activate(self)
72
73
 
74
+ tokens.create! place: workflow.start_place
75
+
73
76
  true
74
77
  end
75
78
  end
@@ -117,5 +120,11 @@ module FlowCore
117
120
  def terminate!(reason:)
118
121
  terminate(reason: reason) || raise(FlowCore::InvalidTransition, "Can't terminate Instance##{id}")
119
122
  end
123
+
124
+ private
125
+
126
+ def on_create_validation
127
+ workflow.on_instance_create_validation(self)
128
+ end
120
129
  end
121
130
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class Pipeline < FlowCore::ApplicationRecord
5
+ self.table_name = "flow_core_pipelines"
6
+
7
+ has_many :generated_workflows,
8
+ class_name: "FlowCore::Workflow", foreign_key: :generated_by_pipeline_id,
9
+ inverse_of: :generated_by, dependent: :nullify
10
+
11
+ has_many :steps, -> { where(branch_id: nil).order(position: :asc) },
12
+ class_name: "FlowCore::Step", inverse_of: :pipeline,
13
+ dependent: :destroy
14
+
15
+ has_many :branches, class_name: "FlowCore::Branch", dependent: :restrict_with_exception
16
+ has_many :whole_steps, class_name: "FlowCore::Step", inverse_of: :pipeline, dependent: :restrict_with_exception
17
+
18
+ validates :name,
19
+ presence: true
20
+
21
+ def deploy_workflow
22
+ return if steps.empty?
23
+ return if whole_steps.exists? verified: false
24
+
25
+ workflow = nil
26
+ transaction do
27
+ workflow = generated_workflows.new name: name, type: workflow_class.to_s
28
+ on_build_workflow(workflow)
29
+ workflow.save!
30
+
31
+ end_place = workflow.create_end_place!
32
+
33
+ place_or_transition = nil
34
+ steps.each do |step|
35
+ place_or_transition = step.deploy_to_workflow!(workflow, place_or_transition)
36
+ end
37
+
38
+ if place_or_transition.is_a? FlowCore::Place
39
+ place_or_transition.input_arcs.update place: end_place
40
+ place_or_transition.reload.destroy!
41
+ else
42
+ end_place.input_transitions << place_or_transition
43
+ end
44
+ end
45
+ workflow&.verify!
46
+
47
+ workflow
48
+ end
49
+
50
+ private
51
+
52
+ def on_build_workflow(_workflow); end
53
+
54
+ def workflow_class
55
+ FlowCore::Workflow
56
+ end
57
+ end
58
+ end
@@ -10,10 +10,11 @@ module FlowCore
10
10
 
11
11
  # NOTE: Place - out -> Transition - in -> Place
12
12
  has_many :input_arcs, -> { where direction: :out },
13
- class_name: "FlowCore::Arc", inverse_of: :place, dependent: :delete_all
13
+ class_name: "FlowCore::Arc", inverse_of: :place, dependent: :destroy
14
14
  has_many :output_arcs, -> { where direction: :in },
15
15
  class_name: "FlowCore::Arc", inverse_of: :place, dependent: :delete_all
16
16
 
17
+ has_many :input_transitions, through: :input_arcs, class_name: "FlowCore::Transition", source: :transition
17
18
  has_many :output_transitions, through: :output_arcs, class_name: "FlowCore::Transition", source: :transition
18
19
 
19
20
  before_destroy :prevent_destroy
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowCore
4
+ class Step < FlowCore::ApplicationRecord
5
+ self.table_name = "flow_core_steps"
6
+
7
+ has_many :generated_transitions,
8
+ class_name: "FlowCore::Transition", foreign_key: :generated_by_step_id,
9
+ inverse_of: :generated_by, dependent: :nullify
10
+
11
+ belongs_to :pipeline, class_name: "FlowCore::Pipeline"
12
+ belongs_to :branch, class_name: "FlowCore::Branch", optional: true
13
+
14
+ has_many :branches, class_name: "FlowCore::Branch", inverse_of: :step, dependent: :destroy
15
+ belongs_to :redirect_to_step, class_name: "FlowCore::Step", optional: true
16
+ has_one :transition_trigger, class_name: "FlowCore::TransitionTrigger", dependent: :destroy
17
+ has_many :transition_callbacks, class_name: "FlowCore::TransitionCallback", dependent: :destroy
18
+
19
+ extend Ancestry::HasAncestry
20
+ has_ancestry orphan_strategy: :restrict
21
+
22
+ extend ActiveRecord::Acts::List::ClassMethods
23
+ acts_as_list scope: %i[pipeline_id branch_id]
24
+
25
+ validates :name,
26
+ presence: true
27
+
28
+ validates :type,
29
+ presence: true
30
+
31
+ validates :redirect_to_step,
32
+ inclusion: {
33
+ in: :redirectable_steps
34
+ },
35
+ allow_nil: true
36
+
37
+ validate do
38
+ if branch && branch.pipeline_id != pipeline_id
39
+ errors.add :branch, :invalid
40
+ end
41
+
42
+ if redirect_to_step && redirect_to_step.pipeline_id != pipeline_id
43
+ errors.add :redirect_to_step, :invalid
44
+ end
45
+
46
+ if redirection_step?
47
+ if !branch
48
+ errors.add :type, :invalid
49
+ elsif branch.fallback_branch?
50
+ errors.add :type, :invalid
51
+ end
52
+ end
53
+ end
54
+
55
+ before_validation do
56
+ self.pipeline ||= branch&.pipeline
57
+ end
58
+
59
+ before_save :update_parent
60
+ before_save :update_verified
61
+
62
+ after_create :reorder_by_append_to_on_create
63
+ after_destroy :recheck_redirection_steps_on_destroy
64
+
65
+ attr_accessor :append_to
66
+
67
+ def deploy_to_workflow!(_workflow, _input_place_or_transition)
68
+ raise NotImplementedError
69
+ end
70
+
71
+ def redirection_step?
72
+ self.class.redirection_step?
73
+ end
74
+
75
+ def multi_branch_step?
76
+ self.class.multi_branch_step?
77
+ end
78
+
79
+ def barrier_step?
80
+ self.class.barrier_step?
81
+ end
82
+
83
+ def transition_trigger_attachable?
84
+ self.class.transition_trigger_attachable?
85
+ end
86
+
87
+ def transition_callback_attachable?
88
+ self.class.transition_callback_attachable?
89
+ end
90
+
91
+ def branch_arc_guard_attachable?
92
+ self.class.branch_arc_guard_attachable?
93
+ end
94
+
95
+ def fallback_branch_required?
96
+ self.class.fallback_branch_required?
97
+ end
98
+
99
+ def transition_trigger_required?
100
+ self.class.transition_trigger_required?
101
+ end
102
+
103
+ def redirection_configurable?
104
+ self.class.redirection_configurable?
105
+ end
106
+
107
+ def branch_configurable?
108
+ self.class.branch_configurable?
109
+ end
110
+
111
+ def redirectable_steps
112
+ return [] unless redirection_step?
113
+
114
+ redirectable_steps = []
115
+
116
+ # Barrier step is the stop point, beyond if can't ensure the workflow valid
117
+ # e.g. consider there's a redirection step in one of branch of a parallel branch step,
118
+ # the parallel branch step is the barrier step, if redirect beyond the barrier step,
119
+ # it will multiplex all branches again and again
120
+ # Barrier step 是保障流程的边界,如果重定向到边界外的节点,就无法保证流程合法,
121
+ # 一个例子是假设并发分支步骤的某个分支里有一个重定向节点,并发分支步骤就是一个 Barrier Step,
122
+ # 如果可以重定向到并发分支步骤外,重定向到步骤后会重新执行这个并发分支步骤,导致其他分支的步骤会被无限重复执行
123
+ ancestors.reverse_each do |step|
124
+ break if step.barrier_step?
125
+
126
+ redirectable_steps << step
127
+ end
128
+
129
+ # return an empty array if there's no safe ancestor,
130
+ # redirect to steps which in current branch in not safe, because it will infinite loop
131
+ # 如果一个安全的祖先步骤都没有,就返回空集,跳到当前分支的步骤会导致死循环或者死代码,没有意义
132
+ return [] if redirectable_steps.empty?
133
+
134
+ # if the redirection step is the first step of a branch,
135
+ # avoiding redirect to parent and ancestors which are first step (of a branch) and without a transition trigger,
136
+ # because it will lead infinite loop,
137
+ # 如果重定向步骤是分支的第一步,那么父步骤和其他也是处于(分支)第一步的祖先,也都要避免跳转,因为会导致死循环
138
+ if position == 1 && redirectable_steps.any?
139
+ if redirectable_steps.first.multi_branch_step? && !redirectable_steps.first.transition_trigger
140
+ redirectable_steps.shift
141
+ end
142
+
143
+ while redirectable_steps.any?
144
+ step = redirectable_steps.first
145
+ if step.multi_branch_step? && !redirectable_steps.first.transition_trigger
146
+ redirectable_steps.shift
147
+
148
+ if step.position > 1
149
+ break
150
+ end
151
+ else
152
+ break
153
+ end
154
+ end
155
+ end
156
+
157
+ # It's safe to redirect to any top level (or main branch) steps,
158
+ # just avoiding the redirect step's ancestor, because it may lead infinite loop
159
+ # 跳到顶层(或者说主干)步骤都是安全的,只是要去掉跳转步骤的祖先,因为会导致死循环
160
+ if redirectable_steps.reject!(&:root?)
161
+ redirectable_steps.concat(pipeline.steps)
162
+ elsif redirectable_steps.empty?
163
+ redirectable_steps.concat(pipeline.steps - ancestors)
164
+ end
165
+
166
+ # It's also safe to redirect to any of children steps,
167
+ # just avoid current branch which the redirection step belongs to.
168
+ # 跳转到任何步骤的分支也都是安全的,就是要避免分支是当前跳转步骤的祖先步骤
169
+ redirectable_steps.map! do |s|
170
+ if s.parent_of? self
171
+ [s].concat s.children.where.not(branch_id: branch_id)
172
+ else
173
+ [s].concat s.children
174
+ end
175
+ end
176
+ redirectable_steps.flatten!
177
+ redirectable_steps.uniq!
178
+ redirectable_steps.reject!(&:redirection_step?)
179
+
180
+ redirectable_steps
181
+ end
182
+
183
+ class << self
184
+ def multi_branch_step?
185
+ false
186
+ end
187
+
188
+ def redirection_step?
189
+ false
190
+ end
191
+
192
+ def barrier_step?
193
+ false
194
+ end
195
+
196
+ def transition_trigger_attachable?
197
+ !multi_branch_step? && !redirection_step?
198
+ end
199
+
200
+ def transition_callback_attachable?
201
+ !multi_branch_step?
202
+ end
203
+
204
+ def branch_arc_guard_attachable?
205
+ false
206
+ end
207
+
208
+ def redirection_configurable?
209
+ false
210
+ end
211
+
212
+ def branch_configurable?
213
+ false
214
+ end
215
+
216
+ def transition_trigger_required?
217
+ false
218
+ end
219
+
220
+ def fallback_branch_required?
221
+ false
222
+ end
223
+ end
224
+
225
+ private
226
+
227
+ def update_verified
228
+ self.verified = valid?
229
+ end
230
+
231
+ def find_or_create_input_place(workflow, input_place_or_transition)
232
+ if input_place_or_transition.is_a? FlowCore::Transition
233
+ input_place_or_transition.output_places.create! workflow: workflow
234
+ elsif input_place_or_transition.is_a? FlowCore::Place
235
+ input_place_or_transition
236
+ elsif workflow.start_place
237
+ workflow.start_place
238
+ else
239
+ workflow.create_start_place!
240
+ end
241
+ end
242
+
243
+ def copy_transition_trigger_to(transition)
244
+ return unless transition_trigger_attachable?
245
+ return unless transition_trigger
246
+
247
+ trigger = transition_trigger.dup
248
+ trigger.transition = transition
249
+ trigger.pipeline = nil
250
+ trigger.step = nil
251
+
252
+ trigger.save!
253
+ end
254
+
255
+ def copy_transition_callbacks_to(transition)
256
+ return unless transition_callback_attachable?
257
+
258
+ transition_callbacks.find_each do |cb|
259
+ new_cb = cb.dup
260
+ new_cb.transition = transition
261
+ new_cb.pipeline = nil
262
+ new_cb.step = nil
263
+ new_cb.save!
264
+ end
265
+ end
266
+
267
+ def deploy_branches_to(workflow, input_transition, append_to_place_or_transition)
268
+ return unless multi_branch_step?
269
+
270
+ branches.includes(:steps, :arc_guards).find_each do |branch|
271
+ if branch.steps.empty?
272
+ if append_to_place_or_transition.is_a?(FlowCore::Place)
273
+ arc = input_transition.output_arcs.create! place: append_to_place_or_transition, fallback_arc: branch.fallback_branch?
274
+ else
275
+ place = workflow.places.create! workflow: workflow
276
+ arc = append_to_place_or_transition.input_arcs.create! place: place, fallback_arc: branch.fallback_branch?
277
+ end
278
+ branch.copy_arc_guards_to arc
279
+
280
+ next
281
+ end
282
+
283
+ place = workflow.places.create! workflow: workflow
284
+ arc = input_transition.output_arcs.create! place: place, fallback_arc: branch.fallback_branch?
285
+ branch.copy_arc_guards_to arc
286
+
287
+ place_or_transition = nil
288
+ branch.steps.each do |step|
289
+ place_or_transition = step.deploy_to_workflow!(workflow, place_or_transition || place)
290
+ end
291
+ next unless place_or_transition
292
+
293
+ if place_or_transition.is_a? FlowCore::Transition
294
+ if append_to_place_or_transition.is_a?(FlowCore::Place)
295
+ place_or_transition.output_places << append_to_place_or_transition
296
+ else
297
+ place = place_or_transition.output_places.create! workflow: workflow
298
+ place.output_transitions << append_to_place_or_transition
299
+ end
300
+ elsif append_to_place_or_transition.is_a?(FlowCore::Place)
301
+ place_or_transition.input_arcs.update place: append_to_place_or_transition
302
+ place_or_transition.reload.destroy!
303
+ else
304
+ place_or_transition.output_places << append_to_place_or_transition
305
+ end
306
+ end
307
+ end
308
+
309
+ def reorder_by_append_to_on_create
310
+ return unless append_to
311
+
312
+ if append_to.to_s == "start"
313
+ move_to_top
314
+ return
315
+ end
316
+
317
+ append_to_step = self.class.find_by id: append_to
318
+ return unless append_to_step && append_to_step.branch_id == branch_id
319
+
320
+ insert_at(append_to_step.position + 1)
321
+ end
322
+
323
+ def recheck_redirection_steps_on_destroy
324
+ return unless branch
325
+
326
+ branch.steps.to_a.select(&:redirection_step?).each do |step|
327
+ unless step.redirectable_steps.include? step.redirect_to_step
328
+ step.update! redirect_to_step: nil
329
+ end
330
+ end
331
+ end
332
+
333
+ def update_parent
334
+ self.parent = branch&.step
335
+ end
336
+ end
337
+ end