orfeas_petri_flow 0.6.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/CHANGELOG.md +80 -0
- data/MIT-LICENSE +22 -0
- data/README.md +592 -0
- data/Rakefile +28 -0
- data/lib/petri_flow/colored/arc_expression.rb +163 -0
- data/lib/petri_flow/colored/color.rb +40 -0
- data/lib/petri_flow/colored/colored_net.rb +146 -0
- data/lib/petri_flow/colored/guard.rb +104 -0
- data/lib/petri_flow/core/arc.rb +63 -0
- data/lib/petri_flow/core/marking.rb +64 -0
- data/lib/petri_flow/core/net.rb +121 -0
- data/lib/petri_flow/core/place.rb +54 -0
- data/lib/petri_flow/core/token.rb +55 -0
- data/lib/petri_flow/core/transition.rb +88 -0
- data/lib/petri_flow/export/cpn_tools_exporter.rb +322 -0
- data/lib/petri_flow/export/json_exporter.rb +224 -0
- data/lib/petri_flow/export/pnml_exporter.rb +229 -0
- data/lib/petri_flow/export/yaml_exporter.rb +246 -0
- data/lib/petri_flow/export.rb +193 -0
- data/lib/petri_flow/generators/adapters/aasm_adapter.rb +69 -0
- data/lib/petri_flow/generators/adapters/state_machines_adapter.rb +83 -0
- data/lib/petri_flow/generators/state_machine_adapter.rb +47 -0
- data/lib/petri_flow/generators/workflow_generator.rb +176 -0
- data/lib/petri_flow/matrix/analyzer.rb +151 -0
- data/lib/petri_flow/matrix/causation.rb +126 -0
- data/lib/petri_flow/matrix/correlation.rb +79 -0
- data/lib/petri_flow/matrix/crud_event_mapping.rb +74 -0
- data/lib/petri_flow/matrix/lineage.rb +113 -0
- data/lib/petri_flow/matrix/reachability.rb +128 -0
- data/lib/petri_flow/railtie.rb +41 -0
- data/lib/petri_flow/registry.rb +85 -0
- data/lib/petri_flow/simulation/simulator.rb +188 -0
- data/lib/petri_flow/simulation/trace.rb +119 -0
- data/lib/petri_flow/tasks/petri_flow.rake +229 -0
- data/lib/petri_flow/verification/boundedness_checker.rb +127 -0
- data/lib/petri_flow/verification/invariant_checker.rb +144 -0
- data/lib/petri_flow/verification/liveness_checker.rb +153 -0
- data/lib/petri_flow/verification/reachability_analyzer.rb +152 -0
- data/lib/petri_flow/verification_runner.rb +287 -0
- data/lib/petri_flow/version.rb +5 -0
- data/lib/petri_flow/visualization/graphviz.rb +220 -0
- data/lib/petri_flow/visualization/mermaid.rb +191 -0
- data/lib/petri_flow/workflow.rb +228 -0
- data/lib/petri_flow.rb +164 -0
- metadata +174 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'export/pnml_exporter'
|
|
4
|
+
require_relative 'export/cpn_tools_exporter'
|
|
5
|
+
require_relative 'export/json_exporter'
|
|
6
|
+
require_relative 'export/yaml_exporter'
|
|
7
|
+
|
|
8
|
+
module PetriFlow
|
|
9
|
+
# Export functionality for Petri nets and Colored Petri Nets
|
|
10
|
+
# Supports multiple formats: PNML, CPN Tools XML, JSON, and YAML
|
|
11
|
+
module Export
|
|
12
|
+
# Export a net to the specified format
|
|
13
|
+
# @param net [Core::Net, Colored::ColoredNet] The net to export
|
|
14
|
+
# @param format [Symbol] The export format (:pnml, :cpn, :json, :yaml)
|
|
15
|
+
# @param options [Hash] Format-specific options
|
|
16
|
+
# @return [String] The exported representation
|
|
17
|
+
def self.export(net, format:, **options)
|
|
18
|
+
exporter = exporter_for(net, format)
|
|
19
|
+
|
|
20
|
+
case format
|
|
21
|
+
when :pnml
|
|
22
|
+
exporter.to_pnml
|
|
23
|
+
when :cpn, :cpn_tools
|
|
24
|
+
exporter.to_cpn
|
|
25
|
+
when :json
|
|
26
|
+
exporter.to_json(**options)
|
|
27
|
+
when :yaml, :yml
|
|
28
|
+
exporter.to_yaml
|
|
29
|
+
else
|
|
30
|
+
raise ArgumentError, "Unknown export format: #{format}. Supported: :pnml, :cpn, :json, :yaml"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Save a net to a file in the specified format
|
|
35
|
+
# @param net [Core::Net, Colored::ColoredNet] The net to export
|
|
36
|
+
# @param filename [String] The output filename
|
|
37
|
+
# @param format [Symbol] The export format (auto-detected from filename if not specified)
|
|
38
|
+
# @param options [Hash] Format-specific options
|
|
39
|
+
def self.save(net, filename, format: nil, **options)
|
|
40
|
+
format ||= detect_format(filename)
|
|
41
|
+
|
|
42
|
+
exporter = exporter_for(net, format)
|
|
43
|
+
|
|
44
|
+
case format
|
|
45
|
+
when :pnml
|
|
46
|
+
exporter.save_pnml(filename)
|
|
47
|
+
when :cpn, :cpn_tools
|
|
48
|
+
exporter.save_cpn(filename)
|
|
49
|
+
when :json
|
|
50
|
+
exporter.save_json(filename, **options)
|
|
51
|
+
when :yaml, :yml
|
|
52
|
+
exporter.save_yaml(filename)
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError, "Unknown export format: #{format}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
filename
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Export a net to hash (useful for JSON/YAML serialization)
|
|
61
|
+
# @param net [Core::Net, Colored::ColoredNet] The net to export
|
|
62
|
+
# @return [Hash] Hash representation of the net
|
|
63
|
+
def self.to_hash(net)
|
|
64
|
+
JsonExporter.new(net).to_hash
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Detect format from filename extension
|
|
68
|
+
# @param filename [String] The filename
|
|
69
|
+
# @return [Symbol] The detected format
|
|
70
|
+
def self.detect_format(filename)
|
|
71
|
+
ext = File.extname(filename).downcase
|
|
72
|
+
case ext
|
|
73
|
+
when '.pnml', '.xml'
|
|
74
|
+
# Check if filename suggests CPN Tools
|
|
75
|
+
if filename.downcase.include?('cpn')
|
|
76
|
+
:cpn
|
|
77
|
+
else
|
|
78
|
+
:pnml
|
|
79
|
+
end
|
|
80
|
+
when '.cpn'
|
|
81
|
+
:cpn
|
|
82
|
+
when '.json'
|
|
83
|
+
:json
|
|
84
|
+
when '.yaml', '.yml'
|
|
85
|
+
:yaml
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError, "Cannot detect format from filename: #{filename}. " \
|
|
88
|
+
"Please specify format explicitly."
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get the appropriate exporter for a net and format
|
|
93
|
+
# @param net [Core::Net, Colored::ColoredNet] The net
|
|
94
|
+
# @param format [Symbol] The export format
|
|
95
|
+
# @return [PnmlExporter, CpnToolsExporter, JsonExporter, YamlExporter]
|
|
96
|
+
def self.exporter_for(net, format)
|
|
97
|
+
case format
|
|
98
|
+
when :pnml
|
|
99
|
+
PnmlExporter.new(net)
|
|
100
|
+
when :cpn, :cpn_tools
|
|
101
|
+
CpnToolsExporter.new(net)
|
|
102
|
+
when :json
|
|
103
|
+
JsonExporter.new(net)
|
|
104
|
+
when :yaml, :yml
|
|
105
|
+
YamlExporter.new(net)
|
|
106
|
+
else
|
|
107
|
+
raise ArgumentError, "Unknown export format: #{format}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# List all available export formats
|
|
112
|
+
# @return [Array<Symbol>] Available formats
|
|
113
|
+
def self.formats
|
|
114
|
+
[:pnml, :cpn, :json, :yaml]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get information about export formats
|
|
118
|
+
# @return [Hash] Format information
|
|
119
|
+
def self.format_info
|
|
120
|
+
{
|
|
121
|
+
pnml: {
|
|
122
|
+
name: 'PNML (Petri Net Markup Language)',
|
|
123
|
+
description: 'ISO/IEC 15909 standard format for Petri nets',
|
|
124
|
+
extension: '.pnml',
|
|
125
|
+
supports_colored: true,
|
|
126
|
+
interoperability: 'High - works with many Petri net tools'
|
|
127
|
+
},
|
|
128
|
+
cpn: {
|
|
129
|
+
name: 'CPN Tools XML',
|
|
130
|
+
description: 'Native format for CPN Tools software',
|
|
131
|
+
extension: '.cpn',
|
|
132
|
+
supports_colored: true,
|
|
133
|
+
interoperability: 'Medium - specific to CPN Tools'
|
|
134
|
+
},
|
|
135
|
+
json: {
|
|
136
|
+
name: 'JSON',
|
|
137
|
+
description: 'Human-readable, API-friendly JSON format',
|
|
138
|
+
extension: '.json',
|
|
139
|
+
supports_colored: true,
|
|
140
|
+
interoperability: 'High - universal format'
|
|
141
|
+
},
|
|
142
|
+
yaml: {
|
|
143
|
+
name: 'YAML',
|
|
144
|
+
description: 'Most human-readable format, ideal for documentation',
|
|
145
|
+
extension: '.yaml',
|
|
146
|
+
supports_colored: true,
|
|
147
|
+
interoperability: 'High - universal format'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Convenience methods added to Net classes
|
|
155
|
+
module PetriFlow
|
|
156
|
+
module Core
|
|
157
|
+
class Net
|
|
158
|
+
# Export this net to a string in the specified format
|
|
159
|
+
def export(format:, **options)
|
|
160
|
+
PetriFlow::Export.export(self, format: format, **options)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Save this net to a file
|
|
164
|
+
def export_to_file(filename, format: nil, **options)
|
|
165
|
+
PetriFlow::Export.save(self, filename, format: format, **options)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Export to hash
|
|
169
|
+
def to_export_hash
|
|
170
|
+
PetriFlow::Export.to_hash(self)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
module Colored
|
|
176
|
+
class ColoredNet
|
|
177
|
+
# Export this colored net to a string in the specified format
|
|
178
|
+
def export(format:, **options)
|
|
179
|
+
PetriFlow::Export.export(self, format: format, **options)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Save this colored net to a file
|
|
183
|
+
def export_to_file(filename, format: nil, **options)
|
|
184
|
+
PetriFlow::Export.save(self, filename, format: format, **options)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Export to hash
|
|
188
|
+
def to_export_hash
|
|
189
|
+
PetriFlow::Export.to_hash(self)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetriFlow
|
|
4
|
+
module Generators
|
|
5
|
+
module Adapters
|
|
6
|
+
# Adapter for AASM (acts_as_state_machine) gem
|
|
7
|
+
# @see https://github.com/aasm/aasm
|
|
8
|
+
class AasmAdapter < StateMachineAdapter
|
|
9
|
+
def self.library_name
|
|
10
|
+
"aasm"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.supports?(model_class)
|
|
14
|
+
model_class.respond_to?(:aasm)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def extract
|
|
18
|
+
aasm = model_class.aasm(state_attribute)
|
|
19
|
+
raise ArgumentError, "No AASM state machine for :#{state_attribute} on #{model_class}" unless aasm
|
|
20
|
+
|
|
21
|
+
states = extract_states(aasm)
|
|
22
|
+
initial = extract_initial_state(aasm)
|
|
23
|
+
transitions = extract_transitions(aasm)
|
|
24
|
+
terminal = find_terminal_states(states, transitions)
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
states: states,
|
|
28
|
+
initial_state: initial,
|
|
29
|
+
terminal_states: terminal,
|
|
30
|
+
transitions: transitions,
|
|
31
|
+
library: self.class.library_name
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def extract_states(aasm)
|
|
38
|
+
aasm.states.map(&:name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_initial_state(aasm)
|
|
42
|
+
aasm.initial_state
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_transitions(aasm)
|
|
46
|
+
transitions = []
|
|
47
|
+
|
|
48
|
+
aasm.events.each do |event|
|
|
49
|
+
event.transitions.each do |transition|
|
|
50
|
+
from_states = Array(transition.from)
|
|
51
|
+
to_state = transition.to
|
|
52
|
+
|
|
53
|
+
from_states.each do |from|
|
|
54
|
+
transitions << {
|
|
55
|
+
name: "#{event.name}_from_#{from}".to_sym,
|
|
56
|
+
event: event.name,
|
|
57
|
+
from: from,
|
|
58
|
+
to: to_state
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
transitions
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetriFlow
|
|
4
|
+
module Generators
|
|
5
|
+
module Adapters
|
|
6
|
+
# Adapter for state_machines-activerecord gem
|
|
7
|
+
# @see https://github.com/state-machines/state_machines-activerecord
|
|
8
|
+
class StateMachinesAdapter < StateMachineAdapter
|
|
9
|
+
def self.library_name
|
|
10
|
+
"state_machines-activerecord"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.supports?(model_class)
|
|
14
|
+
model_class.respond_to?(:state_machines)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def extract
|
|
18
|
+
sm = model_class.state_machines[state_attribute]
|
|
19
|
+
raise ArgumentError, "No state machine for :#{state_attribute} on #{model_class}" unless sm
|
|
20
|
+
|
|
21
|
+
states = extract_states(sm)
|
|
22
|
+
initial = extract_initial_state(sm)
|
|
23
|
+
transitions = extract_transitions(sm)
|
|
24
|
+
terminal = find_terminal_states(states, transitions)
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
states: states,
|
|
28
|
+
initial_state: initial,
|
|
29
|
+
terminal_states: terminal,
|
|
30
|
+
transitions: transitions,
|
|
31
|
+
library: self.class.library_name
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def extract_states(sm)
|
|
38
|
+
sm.states.map(&:name).compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_initial_state(sm)
|
|
42
|
+
# Try to get initial state from a new instance
|
|
43
|
+
initial = sm.initial_state(model_class.new)
|
|
44
|
+
initial.try(:name) || extract_states(sm).first
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def extract_transitions(sm)
|
|
48
|
+
transitions = []
|
|
49
|
+
|
|
50
|
+
sm.events.each do |event|
|
|
51
|
+
event.branches.each do |branch|
|
|
52
|
+
branch.state_requirements.each do |req|
|
|
53
|
+
from_states = extract_state_values(req[:from])
|
|
54
|
+
to_states = extract_state_values(req[:to])
|
|
55
|
+
|
|
56
|
+
from_states.each do |from|
|
|
57
|
+
to_states.each do |to|
|
|
58
|
+
transitions << {
|
|
59
|
+
name: "#{event.name}_from_#{from}".to_sym,
|
|
60
|
+
event: event.name,
|
|
61
|
+
from: from,
|
|
62
|
+
to: to
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
transitions
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extract_state_values(state_matcher)
|
|
74
|
+
if state_matcher.respond_to?(:values)
|
|
75
|
+
state_matcher.values
|
|
76
|
+
else
|
|
77
|
+
[state_matcher]
|
|
78
|
+
end.compact
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetriFlow
|
|
4
|
+
module Generators
|
|
5
|
+
# Base class for state machine adapters.
|
|
6
|
+
# Subclasses extract states and transitions from different state machine libraries.
|
|
7
|
+
#
|
|
8
|
+
# @abstract Subclass and implement {#extract}
|
|
9
|
+
class StateMachineAdapter
|
|
10
|
+
attr_reader :model_class, :state_attribute
|
|
11
|
+
|
|
12
|
+
# @param model_class [Class] ActiveRecord model class
|
|
13
|
+
# @param state_attribute [Symbol] The state attribute name (default: :state)
|
|
14
|
+
def initialize(model_class, state_attribute = :state)
|
|
15
|
+
@model_class = model_class
|
|
16
|
+
@state_attribute = state_attribute.to_sym
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if this adapter can handle the given model
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def self.supports?(model_class)
|
|
22
|
+
raise NotImplementedError, "Subclasses must implement .supports?"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Extract state machine definition
|
|
26
|
+
# @return [Hash] with :states, :initial_state, :terminal_states, :transitions
|
|
27
|
+
def extract
|
|
28
|
+
raise NotImplementedError, "Subclasses must implement #extract"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Human-readable name of the state machine library
|
|
32
|
+
# @return [String]
|
|
33
|
+
def self.library_name
|
|
34
|
+
raise NotImplementedError, "Subclasses must implement .library_name"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
# Find terminal states (states with no outgoing transitions)
|
|
40
|
+
def find_terminal_states(states, transitions)
|
|
41
|
+
states_with_outgoing = Set.new(transitions.map { |t| t[:from] })
|
|
42
|
+
terminal = states.reject { |s| states_with_outgoing.include?(s) }
|
|
43
|
+
terminal.empty? ? [states.last] : terminal
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetriFlow
|
|
4
|
+
module Generators
|
|
5
|
+
# Generates PetriFlow workflows from ActiveRecord state machines.
|
|
6
|
+
#
|
|
7
|
+
# Supports multiple state machine libraries:
|
|
8
|
+
# - state_machines-activerecord (Solidus, many Rails apps)
|
|
9
|
+
# - AASM (acts_as_state_machine)
|
|
10
|
+
#
|
|
11
|
+
# @example Generate a workflow from a model
|
|
12
|
+
# generator = PetriFlow::Generators::WorkflowGenerator.new(Order, :state)
|
|
13
|
+
# generator.generate!
|
|
14
|
+
# # => Creates app/workflows/order_workflow.rb
|
|
15
|
+
#
|
|
16
|
+
# @example Get workflow data without writing file
|
|
17
|
+
# data = generator.extract
|
|
18
|
+
# # => { states: [...], transitions: [...], ... }
|
|
19
|
+
#
|
|
20
|
+
class WorkflowGenerator
|
|
21
|
+
ADAPTERS = [
|
|
22
|
+
Adapters::StateMachinesAdapter,
|
|
23
|
+
Adapters::AasmAdapter
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :model_class, :state_attribute, :adapter
|
|
27
|
+
|
|
28
|
+
# @param model_class [Class] ActiveRecord model class with state machine
|
|
29
|
+
# @param state_attribute [Symbol] The state attribute name (default: :state)
|
|
30
|
+
# @raise [ArgumentError] if no supported state machine is found
|
|
31
|
+
def initialize(model_class, state_attribute = :state)
|
|
32
|
+
@model_class = model_class
|
|
33
|
+
@state_attribute = state_attribute.to_sym
|
|
34
|
+
@adapter = find_adapter
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Extract state machine data
|
|
38
|
+
# @return [Hash] extracted state machine definition
|
|
39
|
+
def extract
|
|
40
|
+
adapter.extract
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Generate workflow class code
|
|
44
|
+
# @return [String] Ruby code for the workflow class
|
|
45
|
+
def generate_code
|
|
46
|
+
data = extract
|
|
47
|
+
generate_workflow_code(data)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Generate workflow file
|
|
51
|
+
# @param output_dir [String, Pathname] directory to write the file
|
|
52
|
+
# @return [String] path to generated file
|
|
53
|
+
def generate!(output_dir: nil)
|
|
54
|
+
output_dir ||= default_output_dir
|
|
55
|
+
FileUtils.mkdir_p(output_dir)
|
|
56
|
+
|
|
57
|
+
filepath = File.join(output_dir, filename)
|
|
58
|
+
File.write(filepath, generate_code)
|
|
59
|
+
filepath
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Build a workflow class dynamically (without writing file)
|
|
63
|
+
# @return [Class] the generated workflow class
|
|
64
|
+
def build_workflow_class
|
|
65
|
+
data = extract
|
|
66
|
+
class_name = workflow_class_name
|
|
67
|
+
|
|
68
|
+
Class.new(PetriFlow::Workflow) do
|
|
69
|
+
workflow_name data[:workflow_name] || "#{class_name} Workflow"
|
|
70
|
+
places(*data[:states])
|
|
71
|
+
initial_place data[:initial_state]
|
|
72
|
+
terminal_places(*data[:terminal_states])
|
|
73
|
+
|
|
74
|
+
data[:transitions].each do |t|
|
|
75
|
+
transition t[:name], from: t[:from], to: t[:to]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Verify the generated workflow
|
|
81
|
+
# @return [Hash] verification results
|
|
82
|
+
def verify
|
|
83
|
+
workflow = build_workflow_class.new
|
|
84
|
+
workflow.verify!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Class name for the generated workflow
|
|
88
|
+
# @return [String]
|
|
89
|
+
def workflow_class_name
|
|
90
|
+
"#{model_class.name.demodulize}Workflow"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Filename for the generated workflow
|
|
94
|
+
# @return [String]
|
|
95
|
+
def filename
|
|
96
|
+
"#{model_class.name.demodulize.underscore}_workflow.rb"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# List of supported state machine libraries
|
|
100
|
+
# @return [Array<String>]
|
|
101
|
+
def self.supported_libraries
|
|
102
|
+
ADAPTERS.map(&:library_name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if a model has a supported state machine
|
|
106
|
+
# @param model_class [Class]
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
def self.supports?(model_class)
|
|
109
|
+
ADAPTERS.any? { |adapter| adapter.supports?(model_class) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Detect which state machine library a model uses
|
|
113
|
+
# @param model_class [Class]
|
|
114
|
+
# @return [String, nil] library name or nil
|
|
115
|
+
def self.detect_library(model_class)
|
|
116
|
+
adapter = ADAPTERS.find { |a| a.supports?(model_class) }
|
|
117
|
+
adapter&.library_name
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def find_adapter
|
|
123
|
+
adapter_class = ADAPTERS.find { |a| a.supports?(model_class) }
|
|
124
|
+
|
|
125
|
+
unless adapter_class
|
|
126
|
+
raise ArgumentError,
|
|
127
|
+
"#{model_class} does not have a supported state machine. " \
|
|
128
|
+
"Supported: #{self.class.supported_libraries.join(', ')}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
adapter_class.new(model_class, state_attribute)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def default_output_dir
|
|
135
|
+
if defined?(Rails)
|
|
136
|
+
Rails.root.join("app", "workflows")
|
|
137
|
+
else
|
|
138
|
+
File.expand_path("app/workflows", Dir.pwd)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def generate_workflow_code(data)
|
|
143
|
+
class_name = workflow_class_name
|
|
144
|
+
workflow_name = "#{model_class.name.demodulize} State Workflow"
|
|
145
|
+
|
|
146
|
+
lines = []
|
|
147
|
+
lines << "# frozen_string_literal: true"
|
|
148
|
+
lines << "# Auto-generated from #{model_class.name} state machine (#{data[:library]})"
|
|
149
|
+
lines << "# Generated at: #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
150
|
+
lines << ""
|
|
151
|
+
lines << "# #{workflow_name}"
|
|
152
|
+
lines << "# Models the state transitions for #{model_class.name}"
|
|
153
|
+
lines << "class #{class_name} < PetriFlow::Workflow"
|
|
154
|
+
lines << " workflow_name #{workflow_name.inspect}"
|
|
155
|
+
lines << ""
|
|
156
|
+
lines << " # States from #{model_class.name}"
|
|
157
|
+
lines << " places #{data[:states].map(&:inspect).join(', ')}"
|
|
158
|
+
lines << " initial_place #{data[:initial_state].inspect}"
|
|
159
|
+
lines << " terminal_places #{data[:terminal_states].map(&:inspect).join(', ')}"
|
|
160
|
+
lines << ""
|
|
161
|
+
lines << " # Transitions from state machine events"
|
|
162
|
+
|
|
163
|
+
data[:transitions].each do |t|
|
|
164
|
+
lines << " transition #{t[:name].inspect},"
|
|
165
|
+
lines << " from: #{t[:from].inspect},"
|
|
166
|
+
lines << " to: #{t[:to].inspect},"
|
|
167
|
+
lines << " trigger: #{t[:event].to_s.inspect}"
|
|
168
|
+
lines << ""
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
lines << "end"
|
|
172
|
+
lines.join("\n")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|