circuit_breaker-wf 0.1.0
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 +7 -0
- data/.gitignore +4 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +116 -0
- data/LICENSE +21 -0
- data/README.md +324 -0
- data/examples/document/README.md +150 -0
- data/examples/document/document_assistant.rb +535 -0
- data/examples/document/document_rules.rb +60 -0
- data/examples/document/document_token.rb +83 -0
- data/examples/document/document_workflow.rb +114 -0
- data/examples/document/mock_executor.rb +80 -0
- data/lib/circuit_breaker/executors/README.md +664 -0
- data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
- data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
- data/lib/circuit_breaker/executors/base_executor.rb +56 -0
- data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
- data/lib/circuit_breaker/executors/dsl.rb +97 -0
- data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
- data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
- data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
- data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
- data/lib/circuit_breaker/executors/step_executor.rb +47 -0
- data/lib/circuit_breaker/history.rb +81 -0
- data/lib/circuit_breaker/rules.rb +251 -0
- data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
- data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
- data/lib/circuit_breaker/token.rb +486 -0
- data/lib/circuit_breaker/visualizer.rb +173 -0
- data/lib/circuit_breaker/workflow_dsl.rb +359 -0
- data/lib/circuit_breaker.rb +236 -0
- data/workflow-editor/.gitignore +24 -0
- data/workflow-editor/README.md +106 -0
- data/workflow-editor/eslint.config.js +28 -0
- data/workflow-editor/index.html +13 -0
- data/workflow-editor/package-lock.json +6864 -0
- data/workflow-editor/package.json +50 -0
- data/workflow-editor/postcss.config.js +6 -0
- data/workflow-editor/public/vite.svg +1 -0
- data/workflow-editor/src/App.css +42 -0
- data/workflow-editor/src/App.tsx +365 -0
- data/workflow-editor/src/assets/react.svg +1 -0
- data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
- data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
- data/workflow-editor/src/components/NodeDetails.tsx +177 -0
- data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
- data/workflow-editor/src/components/SaveButton.tsx +45 -0
- data/workflow-editor/src/config/change_workflow.yaml +59 -0
- data/workflow-editor/src/config/constants.ts +11 -0
- data/workflow-editor/src/config/flowConfig.ts +189 -0
- data/workflow-editor/src/config/uiConfig.ts +77 -0
- data/workflow-editor/src/config/workflow.yaml +58 -0
- data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
- data/workflow-editor/src/index.css +34 -0
- data/workflow-editor/src/main.tsx +10 -0
- data/workflow-editor/src/server/saveWorkflow.ts +81 -0
- data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
- data/workflow-editor/src/utils/workflowLoader.ts +26 -0
- data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
- data/workflow-editor/src/vite-env.d.ts +1 -0
- data/workflow-editor/src/yaml.d.ts +4 -0
- data/workflow-editor/tailwind.config.js +15 -0
- data/workflow-editor/tsconfig.app.json +26 -0
- data/workflow-editor/tsconfig.json +7 -0
- data/workflow-editor/tsconfig.node.json +24 -0
- data/workflow-editor/vite.config.ts +8 -0
- metadata +267 -0
@@ -0,0 +1,359 @@
|
|
1
|
+
module CircuitBreaker
|
2
|
+
module WorkflowBuilder
|
3
|
+
module DSL
|
4
|
+
class ::Symbol
|
5
|
+
def >>(other)
|
6
|
+
StateTransition.new(self, other)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class StateTransition
|
11
|
+
attr_reader :from, :to
|
12
|
+
|
13
|
+
def initialize(from, to)
|
14
|
+
@from = from
|
15
|
+
@to = to
|
16
|
+
end
|
17
|
+
|
18
|
+
def >>(other)
|
19
|
+
StateTransition.new(from, other)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"#{from} -> #{to}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class ActionContext
|
28
|
+
attr_reader :results
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@results = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_result(name, result)
|
35
|
+
@results[name] = result if name
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_result(name)
|
39
|
+
@results[name]
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(name, *args)
|
43
|
+
@results[name]
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to_missing?(name, include_private = false)
|
47
|
+
@results.key?(name) || super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class ActionBuilder
|
52
|
+
attr_reader :context, :actions
|
53
|
+
|
54
|
+
def initialize(workflow_builder)
|
55
|
+
@workflow_builder = workflow_builder
|
56
|
+
@context = ActionContext.new
|
57
|
+
@actions = []
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute(executor, method, result_name = nil, **params)
|
61
|
+
@actions << [executor, method, result_name, params]
|
62
|
+
end
|
63
|
+
|
64
|
+
def execute_actions(token)
|
65
|
+
puts "Executing actions for token #{token.id}..."
|
66
|
+
@actions.each do |executor, method, result_name, params|
|
67
|
+
puts " Executing #{method} -> #{result_name}"
|
68
|
+
result = executor.send(method, token, **params)
|
69
|
+
puts " Result: #{result.inspect}"
|
70
|
+
@context.add_result(result_name, result) if result_name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class StateTransitionBuilder
|
76
|
+
def initialize(workflow_builder, state_transition)
|
77
|
+
@workflow_builder = workflow_builder
|
78
|
+
@from_state = state_transition.from
|
79
|
+
@to_state = state_transition.to
|
80
|
+
end
|
81
|
+
|
82
|
+
def transition(name)
|
83
|
+
@workflow_builder.transition(name, from: @from_state, to: @to_state)
|
84
|
+
end
|
85
|
+
|
86
|
+
def policy(rules)
|
87
|
+
@workflow_builder.policy(rules)
|
88
|
+
end
|
89
|
+
|
90
|
+
def actions(&block)
|
91
|
+
builder = ActionBuilder.new(@workflow_builder)
|
92
|
+
builder.instance_eval(&block)
|
93
|
+
@workflow_builder.set_action_context(builder.context)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class WorkflowBuilder
|
98
|
+
attr_reader :states, :transitions, :before_flows, :rules, :current_token
|
99
|
+
|
100
|
+
def initialize(rules = nil)
|
101
|
+
@states = []
|
102
|
+
@transitions = []
|
103
|
+
@before_flows = []
|
104
|
+
@rules = rules || Rules::DSL.define
|
105
|
+
@current_token = nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def states(*states)
|
109
|
+
@states = states
|
110
|
+
end
|
111
|
+
|
112
|
+
def flow(transition_spec, name = nil, &block)
|
113
|
+
from_state, to_state = parse_transition_spec(transition_spec)
|
114
|
+
raise "Invalid transition spec" unless from_state && to_state
|
115
|
+
|
116
|
+
transition = Transition.new(name || "#{from_state}_to_#{to_state}", from_state, to_state, self)
|
117
|
+
transition.instance_eval(&block) if block_given?
|
118
|
+
@transitions << transition
|
119
|
+
transition
|
120
|
+
end
|
121
|
+
|
122
|
+
def before_flow(&block)
|
123
|
+
@before_flows << block
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_transition(name, from_state = nil)
|
127
|
+
@transitions.find do |t|
|
128
|
+
t.name == name && (from_state.nil? || t.from_state == from_state)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def transition!(token, transition_name)
|
133
|
+
transition = @transitions.find { |t| t.name == transition_name }
|
134
|
+
raise "Invalid transition '#{transition_name}'" unless transition
|
135
|
+
raise "Invalid from state '#{token.state}' (expected #{transition.from_state})" unless token.state.to_s == transition.from_state.to_s
|
136
|
+
|
137
|
+
old_state = token.state
|
138
|
+
@current_token = token # Set current token before executing actions
|
139
|
+
|
140
|
+
begin
|
141
|
+
# Execute actions first
|
142
|
+
transition.execute_actions(token)
|
143
|
+
|
144
|
+
# Then validate rules
|
145
|
+
transition.validate_rules(token, @rules)
|
146
|
+
|
147
|
+
# Finally update state
|
148
|
+
token.state = transition.to_state.to_sym
|
149
|
+
token.record_transition(transition_name, old_state, token.state)
|
150
|
+
|
151
|
+
token
|
152
|
+
ensure
|
153
|
+
@current_token = nil # Clear current token after execution
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def build
|
158
|
+
self # Return self since we're already a workflow
|
159
|
+
end
|
160
|
+
|
161
|
+
def pretty_print
|
162
|
+
puts "States:"
|
163
|
+
puts " #{@states.join(' -> ')}\n\n"
|
164
|
+
|
165
|
+
puts "Transitions:"
|
166
|
+
@transitions.each do |t|
|
167
|
+
puts " #{t.name}: #{t.from_state} -> #{t.to_state}"
|
168
|
+
puts " Rules:"
|
169
|
+
if t.rules.is_a?(Hash)
|
170
|
+
if t.rules[:all]
|
171
|
+
puts " All of:"
|
172
|
+
t.rules[:all].each { |r| puts " - #{r}" }
|
173
|
+
end
|
174
|
+
if t.rules[:any]
|
175
|
+
puts " Any of:"
|
176
|
+
t.rules[:any].each { |r| puts " - #{r}" }
|
177
|
+
end
|
178
|
+
else
|
179
|
+
t.rules.each { |r| puts " - #{r}" }
|
180
|
+
end
|
181
|
+
puts " Actions:"
|
182
|
+
if t.action_builder&.actions
|
183
|
+
t.action_builder.actions.each do |executor, method, result_name, params|
|
184
|
+
result = executor.send(method, @current_token, **params) if @current_token
|
185
|
+
puts " #{result_name}: #{result.inspect}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def parse_transition_spec(spec)
|
194
|
+
case spec
|
195
|
+
when String
|
196
|
+
spec.split(">>").map(&:strip)
|
197
|
+
when Symbol
|
198
|
+
[spec.to_s]
|
199
|
+
when StateTransition
|
200
|
+
[spec.from.to_s, spec.to.to_s]
|
201
|
+
else
|
202
|
+
raise "Invalid transition spec: #{spec}"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
class TransitionBuilder
|
208
|
+
attr_accessor :name, :from_state, :to_state, :rules, :action_context
|
209
|
+
|
210
|
+
def initialize(name, from_state, to_state)
|
211
|
+
@name = name
|
212
|
+
@from_state = from_state
|
213
|
+
@to_state = to_state
|
214
|
+
@rules = []
|
215
|
+
@action_context = nil
|
216
|
+
end
|
217
|
+
|
218
|
+
def build
|
219
|
+
Transition.new(
|
220
|
+
name: @name,
|
221
|
+
from_state: @from_state,
|
222
|
+
to_state: @to_state,
|
223
|
+
rules: @rules,
|
224
|
+
action_context: @action_context
|
225
|
+
)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class Transition
|
230
|
+
attr_reader :name, :from_state, :to_state, :rules, :workflow_builder
|
231
|
+
attr_accessor :action_context, :action_builder
|
232
|
+
|
233
|
+
def initialize(name, from_state, to_state, workflow_builder)
|
234
|
+
@name = name
|
235
|
+
@from_state = from_state
|
236
|
+
@to_state = to_state
|
237
|
+
@rules = []
|
238
|
+
@action_context = nil
|
239
|
+
@action_builder = nil
|
240
|
+
@workflow_builder = workflow_builder
|
241
|
+
end
|
242
|
+
|
243
|
+
def actions(&block)
|
244
|
+
@action_builder = ActionBuilder.new(@workflow_builder)
|
245
|
+
@action_builder.instance_eval(&block) if block_given?
|
246
|
+
@action_context = @action_builder.context
|
247
|
+
end
|
248
|
+
|
249
|
+
def policy(rules)
|
250
|
+
@rules = rules
|
251
|
+
end
|
252
|
+
|
253
|
+
def execute_actions(token)
|
254
|
+
@action_builder&.execute_actions(token)
|
255
|
+
end
|
256
|
+
|
257
|
+
def validate_rules(token, rules_dsl)
|
258
|
+
return unless @rules
|
259
|
+
|
260
|
+
rules_dsl.with_context(@action_context) do
|
261
|
+
# Handle :all rules
|
262
|
+
if @rules[:all]
|
263
|
+
@rules[:all].each do |rule|
|
264
|
+
raise "Rule '#{rule}' failed for transition '#{name}'" unless rules_dsl.evaluate(rule, token)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Handle :any rules
|
269
|
+
if @rules[:any]
|
270
|
+
any_passed = @rules[:any].any? { |rule| rules_dsl.evaluate(rule, token) }
|
271
|
+
raise "None of the rules #{@rules[:any]} passed for transition '#{name}'" unless any_passed
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
module PrettyPrint
|
278
|
+
def pretty_print
|
279
|
+
puts "States:"
|
280
|
+
puts " #{@states.join(' -> ')}"
|
281
|
+
puts "\nTransitions:"
|
282
|
+
@transitions.each do |transition|
|
283
|
+
puts " #{transition.name}: #{transition.from_state} -> #{transition.to_state}"
|
284
|
+
if transition.rules && !transition.rules.empty?
|
285
|
+
puts " Rules:"
|
286
|
+
if transition.rules[:all]
|
287
|
+
puts " All of:"
|
288
|
+
transition.rules[:all].each { |rule| puts " - #{rule}" }
|
289
|
+
end
|
290
|
+
if transition.rules[:any]
|
291
|
+
puts " Any of:"
|
292
|
+
transition.rules[:any].each { |rule| puts " - #{rule}" }
|
293
|
+
end
|
294
|
+
end
|
295
|
+
if transition.action_context && !transition.action_context.results.empty?
|
296
|
+
puts " Actions:"
|
297
|
+
transition.action_context.results.each do |name, result|
|
298
|
+
puts " #{name}: #{result}"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
class Workflow
|
306
|
+
include PrettyPrint
|
307
|
+
|
308
|
+
attr_reader :states, :transitions, :before_flows, :rules, :tokens
|
309
|
+
|
310
|
+
def initialize(states:, transitions:, before_flows:, rules:)
|
311
|
+
@states = states
|
312
|
+
@transitions = transitions
|
313
|
+
@before_flows = before_flows
|
314
|
+
@rules = rules
|
315
|
+
@tokens = []
|
316
|
+
end
|
317
|
+
|
318
|
+
def add_token(token)
|
319
|
+
token.state = @states.first if token.state.nil?
|
320
|
+
@tokens << token
|
321
|
+
end
|
322
|
+
|
323
|
+
def transition!(token, transition_name)
|
324
|
+
transition = @transitions.find { |t| t.name == transition_name }
|
325
|
+
raise "Invalid transition '#{transition_name}'" unless transition
|
326
|
+
raise "Invalid from state '#{token.state}'" unless token.state == transition.from_state
|
327
|
+
|
328
|
+
old_state = token.state
|
329
|
+
@current_token = token # Set current token
|
330
|
+
|
331
|
+
# Then validate rules
|
332
|
+
transition.validate_rules(token, @rules)
|
333
|
+
|
334
|
+
# Execute actions first
|
335
|
+
transition.execute_actions(token)
|
336
|
+
|
337
|
+
# Finally update state
|
338
|
+
token.state = transition.to_state
|
339
|
+
token.record_transition(transition_name, old_state, token.state)
|
340
|
+
|
341
|
+
@current_token = nil # Clear current token
|
342
|
+
token
|
343
|
+
end
|
344
|
+
|
345
|
+
private
|
346
|
+
|
347
|
+
def find_transition(name, current_state)
|
348
|
+
@transitions.find { |t| t.name == name && t.from_state == current_state }
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def self.define(rules: nil, &block)
|
353
|
+
builder = WorkflowBuilder.new(rules)
|
354
|
+
builder.instance_eval(&block)
|
355
|
+
builder.build
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require 'json'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'set'
|
5
|
+
require_relative 'circuit_breaker/version'
|
6
|
+
require_relative 'circuit_breaker/token'
|
7
|
+
require_relative 'circuit_breaker/workflow_dsl'
|
8
|
+
require_relative 'circuit_breaker/rules'
|
9
|
+
|
10
|
+
# Module for implementing a Circuit Breaker pattern using a Petri net
|
11
|
+
module CircuitBreaker
|
12
|
+
# Represents a place in the Petri net workflow
|
13
|
+
# Places can hold tokens and connect to transitions via arcs
|
14
|
+
class Place
|
15
|
+
# @!attribute [r] name
|
16
|
+
# @return [Symbol] Name of the place
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
# @!attribute [r] tokens
|
20
|
+
# @return [Array<Token>] Tokens currently in this place
|
21
|
+
attr_reader :tokens
|
22
|
+
|
23
|
+
# @!attribute [r] input_arcs
|
24
|
+
# @return [Array<Arc>] Input arcs connected to this place
|
25
|
+
attr_reader :input_arcs
|
26
|
+
|
27
|
+
# @!attribute [r] output_arcs
|
28
|
+
# @return [Array<Arc>] Output arcs connected to this place
|
29
|
+
attr_reader :output_arcs
|
30
|
+
|
31
|
+
# Initialize a new place with a given name
|
32
|
+
# @param name [Symbol] Name of the place
|
33
|
+
def initialize(name)
|
34
|
+
@name = name
|
35
|
+
@tokens = []
|
36
|
+
@input_arcs = []
|
37
|
+
@output_arcs = []
|
38
|
+
@mutex = Mutex.new
|
39
|
+
end
|
40
|
+
|
41
|
+
# Add a token to this place
|
42
|
+
# @param token [Token] Token to add
|
43
|
+
def add_token(token)
|
44
|
+
@mutex.synchronize { @tokens << token }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove and return the most recently added token
|
48
|
+
# @return [Token, nil] The removed token or nil if no tokens
|
49
|
+
def remove_token
|
50
|
+
@mutex.synchronize { @tokens.pop }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get the current number of tokens in this place
|
54
|
+
# @return [Integer] Number of tokens
|
55
|
+
def token_count
|
56
|
+
@tokens.size
|
57
|
+
end
|
58
|
+
|
59
|
+
# Add an input arc from a source to this place
|
60
|
+
# @param source [Transition] Source transition
|
61
|
+
# @param weight [Integer] Weight of the arc (default: 1)
|
62
|
+
# @return [Arc] The created arc
|
63
|
+
def add_input_arc(source, weight = 1)
|
64
|
+
arc = Arc.new(source, self, weight)
|
65
|
+
@input_arcs << arc
|
66
|
+
arc
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add an output arc from this place to a target
|
70
|
+
# @param target [Transition] Target transition
|
71
|
+
# @param weight [Integer] Weight of the arc (default: 1)
|
72
|
+
# @return [Arc] The created arc
|
73
|
+
def add_output_arc(target, weight = 1)
|
74
|
+
arc = Arc.new(self, target, weight)
|
75
|
+
@output_arcs << arc
|
76
|
+
arc
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Represents an arc connecting places and transitions in the Petri net
|
81
|
+
# Arcs can have weights to specify how many tokens are required/produced
|
82
|
+
class Arc
|
83
|
+
# @!attribute [r] weight
|
84
|
+
# @return [Integer] Weight of the arc
|
85
|
+
attr_reader :weight
|
86
|
+
|
87
|
+
# @!attribute [r] source
|
88
|
+
# @return [Place, Transition] Source node
|
89
|
+
attr_reader :source
|
90
|
+
|
91
|
+
# @!attribute [r] target
|
92
|
+
# @return [Place, Transition] Target node
|
93
|
+
attr_reader :target
|
94
|
+
|
95
|
+
# Initialize a new arc
|
96
|
+
# @param source [Place, Transition] Source node
|
97
|
+
# @param target [Place, Transition] Target node
|
98
|
+
# @param weight [Integer] Weight of the arc (default: 1)
|
99
|
+
def initialize(source, target, weight = 1)
|
100
|
+
@source = source
|
101
|
+
@target = target
|
102
|
+
@weight = weight
|
103
|
+
end
|
104
|
+
|
105
|
+
# Check if this arc is enabled (can fire)
|
106
|
+
# @return [Boolean] True if the arc can fire
|
107
|
+
def enabled?
|
108
|
+
case source
|
109
|
+
when Place
|
110
|
+
source.token_count >= weight
|
111
|
+
when Transition
|
112
|
+
true # Transitions can always produce tokens
|
113
|
+
else
|
114
|
+
false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Represents a transition in the Petri net workflow
|
120
|
+
# Transitions connect places and can have guard conditions
|
121
|
+
class Transition
|
122
|
+
attr_reader :name, :from_state, :to_state, :input_arcs, :output_arcs, :guard
|
123
|
+
|
124
|
+
def initialize(name:, from:, to:)
|
125
|
+
@name = name
|
126
|
+
@from_state = from
|
127
|
+
@to_state = to
|
128
|
+
@input_arcs = []
|
129
|
+
@output_arcs = []
|
130
|
+
@guard = nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def add_input_arc(place, weight = 1)
|
134
|
+
arc = Arc.new(place, self, weight)
|
135
|
+
@input_arcs << arc
|
136
|
+
arc
|
137
|
+
end
|
138
|
+
|
139
|
+
def add_output_arc(place, weight = 1)
|
140
|
+
arc = Arc.new(self, place, weight)
|
141
|
+
@output_arcs << arc
|
142
|
+
arc
|
143
|
+
end
|
144
|
+
|
145
|
+
def set_guard(&block)
|
146
|
+
@guard = block
|
147
|
+
end
|
148
|
+
|
149
|
+
def enabled?
|
150
|
+
return false unless @guard.nil? || @guard.call
|
151
|
+
@input_arcs.all?(&:enabled?) && @output_arcs.all?(&:enabled?)
|
152
|
+
end
|
153
|
+
|
154
|
+
def can_fire?(token)
|
155
|
+
return true unless @guard
|
156
|
+
@guard.call(token)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Main workflow class that manages the Petri net
|
161
|
+
# Can be created from a configuration or built programmatically
|
162
|
+
class Workflow
|
163
|
+
attr_reader :places, :transitions, :tokens, :rules
|
164
|
+
|
165
|
+
def initialize(states: [], transitions: {}, before_flows: [], rules: [])
|
166
|
+
@places = {}
|
167
|
+
@transitions = {}
|
168
|
+
@tokens = Set.new
|
169
|
+
@rules = rules
|
170
|
+
@before_flows = before_flows
|
171
|
+
|
172
|
+
self.states = states
|
173
|
+
transitions.each do |transition, data|
|
174
|
+
add_transition(transition, data[:from], data[:to])
|
175
|
+
@transitions[transition].set_guard do |token|
|
176
|
+
data[:rules]&.each { |rule| return false unless @rules.evaluate(rule, token) }
|
177
|
+
true
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def states=(state_list)
|
183
|
+
state_list.each do |state|
|
184
|
+
@places[state] = Place.new(state)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def add_token(token)
|
189
|
+
token.state = @places.keys.first if token.state.nil?
|
190
|
+
@tokens.add(token)
|
191
|
+
@places[token.state].add_token(token)
|
192
|
+
end
|
193
|
+
|
194
|
+
def fire_transition(transition_name, token)
|
195
|
+
transition = @transitions[transition_name]
|
196
|
+
raise "Invalid transition: #{transition_name}" unless transition
|
197
|
+
raise "Invalid state: #{token.state}" unless @places[token.state]
|
198
|
+
raise "Token not in workflow" unless @tokens.include?(token)
|
199
|
+
|
200
|
+
# Run any before_flow blocks
|
201
|
+
@before_flows.each { |block| block.call(token) }
|
202
|
+
|
203
|
+
# Check if transition is valid
|
204
|
+
unless transition.from_state == token.state
|
205
|
+
raise "Cannot fire transition '#{transition_name}' from state '#{token.state}'"
|
206
|
+
end
|
207
|
+
|
208
|
+
# Try to fire the transition
|
209
|
+
unless transition.can_fire?(token)
|
210
|
+
raise "Transition '#{transition_name}' cannot fire"
|
211
|
+
end
|
212
|
+
|
213
|
+
# Move token to new state
|
214
|
+
@places[token.state].remove_token
|
215
|
+
old_state = token.state
|
216
|
+
token.state = transition.to_state
|
217
|
+
@places[token.state].add_token(token)
|
218
|
+
|
219
|
+
# Record the transition
|
220
|
+
token.record_transition(transition_name, old_state, token.state)
|
221
|
+
|
222
|
+
token
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
def add_transition(name, from, to)
|
228
|
+
transition = Transition.new(name: name, from: from, to: to)
|
229
|
+
@transitions[name] = transition
|
230
|
+
|
231
|
+
# Add arcs
|
232
|
+
@places[from].add_output_arc(transition)
|
233
|
+
transition.add_output_arc(@places[to])
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Logs
|
2
|
+
logs
|
3
|
+
*.log
|
4
|
+
npm-debug.log*
|
5
|
+
yarn-debug.log*
|
6
|
+
yarn-error.log*
|
7
|
+
pnpm-debug.log*
|
8
|
+
lerna-debug.log*
|
9
|
+
|
10
|
+
node_modules
|
11
|
+
dist
|
12
|
+
dist-ssr
|
13
|
+
*.local
|
14
|
+
|
15
|
+
# Editor directories and files
|
16
|
+
.vscode/*
|
17
|
+
!.vscode/extensions.json
|
18
|
+
.idea
|
19
|
+
.DS_Store
|
20
|
+
*.suo
|
21
|
+
*.ntvs*
|
22
|
+
*.njsproj
|
23
|
+
*.sln
|
24
|
+
*.sw?
|