state_machines-diagram 0.1.0 → 0.2.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '03019279aa49c2b91412b51dec857605336d30b20badb3de3ae33478343689c4'
|
|
4
|
+
data.tar.gz: 5bd3185cd7c949225c77183fb6318becf60a44cdae689a1ac5c08a41ac9b2c03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 600b7969ba70e92c709feaf262467819216b1a5ad23dbb03e5f3327eb808f233661d91a44fbf871aaf8e8c5a177745705c6463705a321867c20bf9ed4503f3d4
|
|
7
|
+
data.tar.gz: a9559beefddd3ba6dac1505777bfa93f2a024fce034237771ea59fcb528e7f34c0768062fca4a8b972fb17e5d887923bf5240c98b99cc220d38d010d4161c47d
|
|
@@ -7,6 +7,17 @@ module StateMachines
|
|
|
7
7
|
class Builder
|
|
8
8
|
attr_reader :machine, :options, :diagram, :state_metadata, :transition_metadata
|
|
9
9
|
|
|
10
|
+
# Human-readable label for a Proc/Method, pointing at its source location
|
|
11
|
+
def self.callable_label(callable)
|
|
12
|
+
if callable.respond_to?(:source_location) && callable.source_location
|
|
13
|
+
file, line = callable.source_location
|
|
14
|
+
filename = File.basename(file) if file
|
|
15
|
+
"lambda@#{filename}:#{line}"
|
|
16
|
+
else
|
|
17
|
+
'lambda'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
10
21
|
def initialize(machine, options = {})
|
|
11
22
|
@machine = machine
|
|
12
23
|
@options = options
|
|
@@ -231,13 +242,7 @@ module StateMachines
|
|
|
231
242
|
method_name += '?' unless method_name.end_with?('?')
|
|
232
243
|
method_name
|
|
233
244
|
when Proc, Method
|
|
234
|
-
|
|
235
|
-
file, line = condition.source_location
|
|
236
|
-
filename = File.basename(file) if file
|
|
237
|
-
"lambda@#{filename}:#{line}"
|
|
238
|
-
else
|
|
239
|
-
'lambda'
|
|
240
|
-
end
|
|
245
|
+
self.class.callable_label(condition)
|
|
241
246
|
else
|
|
242
247
|
condition.to_s
|
|
243
248
|
end
|
|
@@ -266,16 +271,7 @@ module StateMachines
|
|
|
266
271
|
def format_action(action)
|
|
267
272
|
case action
|
|
268
273
|
when Proc, Method
|
|
269
|
-
|
|
270
|
-
if action.respond_to?(:source_location) && action.source_location
|
|
271
|
-
file, line = action.source_location
|
|
272
|
-
filename = File.basename(file) if file
|
|
273
|
-
"lambda@#{filename}:#{line}"
|
|
274
|
-
else
|
|
275
|
-
'lambda'
|
|
276
|
-
end
|
|
277
|
-
when Symbol
|
|
278
|
-
action.to_s
|
|
274
|
+
self.class.callable_label(action)
|
|
279
275
|
else
|
|
280
276
|
action.to_s
|
|
281
277
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'builder'
|
|
4
|
+
require_relative 'schema_builder'
|
|
4
5
|
require 'set'
|
|
5
6
|
|
|
6
7
|
module StateMachines
|
|
@@ -20,6 +21,10 @@ module StateMachines
|
|
|
20
21
|
[diagram, builder]
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
def machine_schema(machine, options = {})
|
|
25
|
+
SchemaBuilder.new(machine, options).build
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
def output_diagram(diagram, io, options, builder = nil)
|
|
24
29
|
case options[:format]
|
|
25
30
|
when :json
|
|
@@ -27,6 +32,10 @@ module StateMachines
|
|
|
27
32
|
when :yaml
|
|
28
33
|
require 'yaml'
|
|
29
34
|
io.puts diagram_hash_with_metadata(diagram, builder).to_yaml
|
|
35
|
+
when :machine_schema
|
|
36
|
+
# Output MachineSchema format compatible with state-machines-rs CLI
|
|
37
|
+
schema = SchemaBuilder.new(builder.machine, options)
|
|
38
|
+
io.puts schema.to_json
|
|
30
39
|
else
|
|
31
40
|
# Default text representation
|
|
32
41
|
io.puts diagram_to_text(diagram, options, builder)
|
|
@@ -57,13 +66,7 @@ module StateMachines
|
|
|
57
66
|
end
|
|
58
67
|
|
|
59
68
|
# Build diagram with all involved states
|
|
60
|
-
builder =
|
|
61
|
-
builder.instance_variable_set(:@diagram, builder.send(:create_diagram))
|
|
62
|
-
|
|
63
|
-
# Add all involved states first
|
|
64
|
-
machine.states.select { |s| states_involved.include?(s.name) }.each do |s|
|
|
65
|
-
builder.send(:add_state_node, s)
|
|
66
|
-
end
|
|
69
|
+
builder = build_filtered_builder(machine, states_involved, options)
|
|
67
70
|
|
|
68
71
|
# Then add transitions
|
|
69
72
|
machine.events.each do |event|
|
|
@@ -138,13 +141,7 @@ module StateMachines
|
|
|
138
141
|
end
|
|
139
142
|
|
|
140
143
|
# Build a fresh diagram for this event
|
|
141
|
-
builder =
|
|
142
|
-
builder.instance_variable_set(:@diagram, builder.send(:create_diagram))
|
|
143
|
-
|
|
144
|
-
# Add states to diagram first
|
|
145
|
-
machine.states.select { |s| states_involved.include?(s.name) }.each do |state|
|
|
146
|
-
builder.send(:add_state_node, state)
|
|
147
|
-
end
|
|
144
|
+
builder = build_filtered_builder(machine, states_involved, options)
|
|
148
145
|
|
|
149
146
|
# Add transitions for this event
|
|
150
147
|
builder.send(:add_event_transitions, event)
|
|
@@ -166,18 +163,24 @@ module StateMachines
|
|
|
166
163
|
end
|
|
167
164
|
|
|
168
165
|
# Build fresh diagram
|
|
169
|
-
builder =
|
|
166
|
+
builder = build_filtered_builder(machine, states_involved)
|
|
167
|
+
|
|
168
|
+
# Add transitions
|
|
169
|
+
builder.send(:add_branch_transitions, branch, event)
|
|
170
|
+
|
|
171
|
+
output_diagram(builder.diagram, io, {}, builder)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Creates a fresh builder whose diagram only contains the given states
|
|
175
|
+
def build_filtered_builder(machine, states_involved, options = {})
|
|
176
|
+
builder = Builder.new(machine, options)
|
|
170
177
|
builder.instance_variable_set(:@diagram, builder.send(:create_diagram))
|
|
171
178
|
|
|
172
|
-
# Add states first
|
|
173
179
|
machine.states.select { |s| states_involved.include?(s.name) }.each do |state|
|
|
174
180
|
builder.send(:add_state_node, state)
|
|
175
181
|
end
|
|
176
182
|
|
|
177
|
-
|
|
178
|
-
builder.send(:add_branch_transitions, branch, event)
|
|
179
|
-
|
|
180
|
-
output_diagram(builder.diagram, io, {}, builder)
|
|
183
|
+
builder
|
|
181
184
|
end
|
|
182
185
|
|
|
183
186
|
def diagram_to_text(diagram, options = {}, builder = nil)
|
|
@@ -284,11 +287,10 @@ module StateMachines
|
|
|
284
287
|
end
|
|
285
288
|
|
|
286
289
|
conditions = metadata.fetch(:conditions, {})
|
|
287
|
-
|
|
288
|
-
Array(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
Array(condition).each { |item| guard_terms[:unless] << normalize_condition_name(item) }
|
|
290
|
+
%i[if unless].each do |kind|
|
|
291
|
+
Array(conditions[kind]).compact.each do |condition|
|
|
292
|
+
Array(condition).each { |item| guard_terms[kind] << normalize_condition_name(item) }
|
|
293
|
+
end
|
|
292
294
|
end
|
|
293
295
|
|
|
294
296
|
guard_terms[:if].uniq!
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
module Diagram
|
|
5
|
+
# Builds a MachineSchema-compatible hash from a state_machines machine.
|
|
6
|
+
# This format is compatible with the state-machines-rs CLI tool.
|
|
7
|
+
class SchemaBuilder
|
|
8
|
+
attr_reader :machine, :options
|
|
9
|
+
|
|
10
|
+
def initialize(machine, options = {})
|
|
11
|
+
@machine = machine
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build
|
|
16
|
+
{
|
|
17
|
+
name: machine_name,
|
|
18
|
+
initial: initial_state,
|
|
19
|
+
states: state_names,
|
|
20
|
+
superstates: build_superstates,
|
|
21
|
+
events: build_events,
|
|
22
|
+
async_mode: false
|
|
23
|
+
}.compact
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_json(*args)
|
|
27
|
+
build.to_json(*args)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def machine_name
|
|
33
|
+
"#{machine.owner_class.name}##{machine.name}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initial_state
|
|
37
|
+
state = machine.initial_state(machine.owner_class)
|
|
38
|
+
if state.respond_to?(:name)
|
|
39
|
+
state.name&.to_s
|
|
40
|
+
else
|
|
41
|
+
state&.to_s
|
|
42
|
+
end || machine.states.first&.name&.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def state_names
|
|
46
|
+
machine.states.by_priority.map { |s| s.name&.to_s || 'nil' }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_superstates
|
|
50
|
+
# state_machines gem doesn't have native superstate support
|
|
51
|
+
# Return empty array for compatibility
|
|
52
|
+
[]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_events
|
|
56
|
+
machine.events.map do |event|
|
|
57
|
+
{
|
|
58
|
+
name: event.name.to_s,
|
|
59
|
+
transitions: build_event_transitions(event),
|
|
60
|
+
guards: extract_event_guards(event),
|
|
61
|
+
payload: nil
|
|
62
|
+
}.tap do |h|
|
|
63
|
+
h.delete(:guards) if h[:guards].empty?
|
|
64
|
+
h.delete(:payload) if h[:payload].nil?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_event_transitions(event)
|
|
70
|
+
transitions = []
|
|
71
|
+
|
|
72
|
+
event.branches.each do |branch|
|
|
73
|
+
branch.state_requirements.each do |requirement|
|
|
74
|
+
valid_states = machine.states.by_priority.map(&:name)
|
|
75
|
+
from_states = requirement[:from].filter(valid_states)
|
|
76
|
+
to_states = requirement[:to].values
|
|
77
|
+
|
|
78
|
+
# Handle loopback transitions (empty to_states means same state)
|
|
79
|
+
to_states = from_states if to_states.empty?
|
|
80
|
+
|
|
81
|
+
to_states.each do |to_state|
|
|
82
|
+
transitions << {
|
|
83
|
+
sources: from_states.map { |s| s&.to_s || 'nil' },
|
|
84
|
+
target: to_state&.to_s || 'nil',
|
|
85
|
+
guards: extract_branch_guards(branch),
|
|
86
|
+
unless: extract_branch_unless(branch)
|
|
87
|
+
}.tap do |h|
|
|
88
|
+
h.delete(:guards) if h[:guards].empty?
|
|
89
|
+
h.delete(:unless) if h[:unless].empty?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Merge transitions with same target
|
|
96
|
+
merge_transitions(transitions)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def merge_transitions(transitions)
|
|
100
|
+
# Group by target and merge sources
|
|
101
|
+
grouped = transitions.group_by { |t| [t[:target], t[:guards], t[:unless]] }
|
|
102
|
+
|
|
103
|
+
grouped.map do |(_target, _guards, _unless), group|
|
|
104
|
+
sources = group.flat_map { |t| t[:sources] }.uniq
|
|
105
|
+
base = group.first.dup
|
|
106
|
+
base[:sources] = sources
|
|
107
|
+
base
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def extract_event_guards(event)
|
|
112
|
+
guards = []
|
|
113
|
+
event.branches.each do |branch|
|
|
114
|
+
guards.concat(extract_branch_guards(branch))
|
|
115
|
+
end
|
|
116
|
+
guards.uniq
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def extract_branch_guards(branch)
|
|
120
|
+
guards = []
|
|
121
|
+
if branch.if_condition
|
|
122
|
+
guards << normalize_condition(branch.if_condition)
|
|
123
|
+
end
|
|
124
|
+
guards.compact
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def extract_branch_unless(branch)
|
|
128
|
+
guards = []
|
|
129
|
+
if branch.unless_condition
|
|
130
|
+
guards << normalize_condition(branch.unless_condition)
|
|
131
|
+
end
|
|
132
|
+
guards.compact
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def normalize_condition(condition)
|
|
136
|
+
case condition
|
|
137
|
+
when Symbol
|
|
138
|
+
name = condition.to_s
|
|
139
|
+
name += '?' unless name.end_with?('?')
|
|
140
|
+
name
|
|
141
|
+
when Proc, Method
|
|
142
|
+
if condition.respond_to?(:source_location) && condition.source_location
|
|
143
|
+
file, line = condition.source_location
|
|
144
|
+
"lambda@#{File.basename(file)}:#{line}"
|
|
145
|
+
else
|
|
146
|
+
'lambda'
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
condition.to_s
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: state_machines-diagram
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -63,14 +63,14 @@ dependencies:
|
|
|
63
63
|
requirements:
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '
|
|
66
|
+
version: '6.0'
|
|
67
67
|
type: :development
|
|
68
68
|
prerelease: false
|
|
69
69
|
version_requirements: !ruby/object:Gem::Requirement
|
|
70
70
|
requirements:
|
|
71
71
|
- - "~>"
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '
|
|
73
|
+
version: '6.0'
|
|
74
74
|
- !ruby/object:Gem::Dependency
|
|
75
75
|
name: rake
|
|
76
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -98,6 +98,7 @@ files:
|
|
|
98
98
|
- lib/state_machines-diagram.rb
|
|
99
99
|
- lib/state_machines/diagram/builder.rb
|
|
100
100
|
- lib/state_machines/diagram/renderer.rb
|
|
101
|
+
- lib/state_machines/diagram/schema_builder.rb
|
|
101
102
|
- lib/state_machines/diagram/version.rb
|
|
102
103
|
homepage: https://github.com/state-machines/state_machines-diagram
|
|
103
104
|
licenses:
|
|
@@ -111,14 +112,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
111
112
|
requirements:
|
|
112
113
|
- - ">="
|
|
113
114
|
- !ruby/object:Gem::Version
|
|
114
|
-
version:
|
|
115
|
+
version: '4.0'
|
|
115
116
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
117
|
requirements:
|
|
117
118
|
- - ">="
|
|
118
119
|
- !ruby/object:Gem::Version
|
|
119
120
|
version: '0'
|
|
120
121
|
requirements: []
|
|
121
|
-
rubygems_version:
|
|
122
|
+
rubygems_version: 4.0.20
|
|
122
123
|
specification_version: 4
|
|
123
124
|
summary: Diagram building for state machines
|
|
124
125
|
test_files: []
|