flow_core 0.0.3 → 0.0.5

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 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