state_machines-mermaid 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/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/lib/state_machines/mermaid/renderer.rb +192 -0
- data/lib/state_machines/mermaid/version.rb +7 -0
- data/lib/state_machines-mermaid.rb +9 -0
- data/test/mermaid_renderer_test.rb +208 -0
- data/test/support/models/battle.rb +190 -0
- data/test/support/models/character.rb +183 -0
- data/test/support/models/commander.rb +23 -0
- data/test/support/models/dragon.rb +237 -0
- data/test/support/models/mage.rb +261 -0
- data/test/support/models/regiment.rb +244 -0
- data/test/support/models/troll.rb +167 -0
- data/test/test_helper.rb +17 -0
- metadata +162 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 387fad10965ac7756257b30ac3a36b20770c2702631796c2727c3b0cb4eeea69
|
|
4
|
+
data.tar.gz: 26d45114695fab1ed434862f36520c4006f895d5db17903519995366e6045090
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c92e86302c88b96d81dded14771366d26225970a915e9077d0b71311fdd02cdc4e4b2a068311afbc1fca8e951e018d04fb4ed39f5e311c92035316e1acf97bb2
|
|
7
|
+
data.tar.gz: bb1ce73610a01f2892c2dc1b7737eff292b851e2a988e20a0bf13628a5c28e24a492cc9c6065714adb7278da03b7574354264f96c8a8619d714b143e5df032b3
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Abdelkader Boudih
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# StateMachines::Mermaid
|
|
2
|
+
|
|
3
|
+
[](https://github.com/state-machines/state_machines-mermaid/actions/workflows/ruby.yml)
|
|
4
|
+
[](https://rubygems.org/gems/state_machines-mermaid)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
|
|
7
|
+
Mermaid diagram renderer for [state_machines](https://github.com/state-machines/state_machines). Generate Mermaid state diagrams from your state machines.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add this line to your application's Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'state_machines-mermaid'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
And then execute:
|
|
18
|
+
|
|
19
|
+
$ bundle install
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
$ gem install state_machines-mermaid
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Basic Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'state_machines-mermaid'
|
|
31
|
+
|
|
32
|
+
class Order
|
|
33
|
+
state_machine :status, initial: :pending do
|
|
34
|
+
event :process do
|
|
35
|
+
transition pending: :processing
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
event :ship do
|
|
39
|
+
transition processing: :shipped
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
event :deliver do
|
|
43
|
+
transition shipped: :delivered
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
event :cancel do
|
|
47
|
+
transition [:pending, :processing] => :cancelled
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
state :delivered, :cancelled do
|
|
51
|
+
def final?
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Generate Mermaid diagram
|
|
59
|
+
puts Order.state_machine(:status).draw
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This generates:
|
|
63
|
+
|
|
64
|
+
```mermaid
|
|
65
|
+
stateDiagram-v2
|
|
66
|
+
pending : pending
|
|
67
|
+
processing : processing
|
|
68
|
+
shipped : shipped
|
|
69
|
+
delivered : delivered
|
|
70
|
+
cancelled : cancelled
|
|
71
|
+
pending --> processing : process
|
|
72
|
+
processing --> shipped : ship
|
|
73
|
+
shipped --> delivered : deliver
|
|
74
|
+
pending --> cancelled : cancel
|
|
75
|
+
processing --> cancelled : cancel
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### With Conditions
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
class Character
|
|
82
|
+
state_machine :status, initial: :idle do
|
|
83
|
+
event :attack do
|
|
84
|
+
transition idle: :combat, if: :has_weapon?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
event :rest do
|
|
88
|
+
transition combat: :idle, unless: :in_danger?
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Generates transitions with conditions
|
|
94
|
+
puts Character.state_machine(:status).draw
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Output includes conditions:
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
stateDiagram-v2
|
|
101
|
+
idle : idle
|
|
102
|
+
combat : combat
|
|
103
|
+
idle --> combat : attack [if has_weapon?]
|
|
104
|
+
combat --> idle : rest [unless in_danger?]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Show Callbacks
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Include callback information in the diagram
|
|
111
|
+
Character.state_machine(:status).draw(show_callbacks: true)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Output to File
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
File.open('state_diagram.mmd', 'w') do |file|
|
|
118
|
+
Order.state_machine(:status).draw(io: file)
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Integration with Mermaid Tools
|
|
123
|
+
|
|
124
|
+
The generated output is compatible with:
|
|
125
|
+
|
|
126
|
+
- [Mermaid Live Editor](https://mermaid-js.github.io/mermaid-live-editor/)
|
|
127
|
+
- GitHub Markdown (renders Mermaid diagrams natively)
|
|
128
|
+
- Various documentation tools (GitLab, Notion, etc.)
|
|
129
|
+
- Mermaid CLI for generating PNG/SVG files
|
|
130
|
+
|
|
131
|
+
### Example: Complex State Machine
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
class Dragon
|
|
135
|
+
state_machine :mood, initial: :sleeping do
|
|
136
|
+
state :sleeping, :hunting, :hoarding, :rampaging
|
|
137
|
+
|
|
138
|
+
event :wake_up do
|
|
139
|
+
transition sleeping: :hunting, if: :hungry?
|
|
140
|
+
transition sleeping: :hoarding, if: :treasure_nearby?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
event :find_treasure do
|
|
144
|
+
transition hunting: :hoarding
|
|
145
|
+
transition hoarding: same # Keep hoarding
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
event :enrage do
|
|
149
|
+
transition any - :rampaging => :rampaging
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
puts Dragon.state_machine(:mood).draw
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Features
|
|
158
|
+
|
|
159
|
+
- Generates valid Mermaid state diagram syntax
|
|
160
|
+
- Supports initial and final states
|
|
161
|
+
- Shows transition conditions (if/unless)
|
|
162
|
+
- Handles self-transitions (loopbacks)
|
|
163
|
+
- Sanitizes state names for Mermaid compatibility
|
|
164
|
+
- Optional callback visualization
|
|
165
|
+
- Compatible with all state_machines features
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
170
|
+
|
|
171
|
+
## Contributing
|
|
172
|
+
|
|
173
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/state-machines/state_machines-mermaid.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'state_machines-diagram'
|
|
4
|
+
require 'mermaid'
|
|
5
|
+
|
|
6
|
+
module StateMachines
|
|
7
|
+
module Mermaid
|
|
8
|
+
module Renderer
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# Cache recent metadata so we can enhance the Mermaid output with
|
|
12
|
+
# condition/callback details when the caller requests it.
|
|
13
|
+
def reset_metadata!
|
|
14
|
+
@last_state_metadata = nil
|
|
15
|
+
@last_transition_metadata = nil
|
|
16
|
+
@last_transition_metadata_map = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The new simplified approach - leverages the diagram gem's semantic structure
|
|
20
|
+
def draw_machine(machine, io: $stdout, **options)
|
|
21
|
+
reset_metadata!
|
|
22
|
+
diagram = build_state_diagram(machine, options)
|
|
23
|
+
output_diagram(diagram, io, options)
|
|
24
|
+
diagram
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_state_diagram(machine, options)
|
|
28
|
+
builder = StateMachines::Diagram::Builder.new(machine, options)
|
|
29
|
+
diagram = builder.build
|
|
30
|
+
|
|
31
|
+
@last_state_metadata = builder.state_metadata
|
|
32
|
+
@last_transition_metadata = builder.transition_metadata
|
|
33
|
+
@last_transition_metadata_map = nil
|
|
34
|
+
|
|
35
|
+
diagram
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def draw_state(state, graph, options = {}, io = $stdout)
|
|
39
|
+
reset_metadata!
|
|
40
|
+
diagram = build_state_diagram(state.machine, options)
|
|
41
|
+
mermaid_syntax = render_mermaid(diagram, options)
|
|
42
|
+
# Filter to show only relevant transitions for this state
|
|
43
|
+
filtered_lines = filter_mermaid_for_state(mermaid_syntax, state.name.to_s)
|
|
44
|
+
io.puts filtered_lines
|
|
45
|
+
diagram
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def draw_event(event, graph, options = {}, io = $stdout)
|
|
49
|
+
reset_metadata!
|
|
50
|
+
diagram = build_state_diagram(event.machine, options)
|
|
51
|
+
mermaid_syntax = render_mermaid(diagram, options)
|
|
52
|
+
# Filter to show only transitions triggered by this event
|
|
53
|
+
filtered_lines = filter_mermaid_for_event(mermaid_syntax, event.name.to_s)
|
|
54
|
+
io.puts filtered_lines
|
|
55
|
+
diagram
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The core method - just delegates to the mermaid gem's to_mermaid method
|
|
59
|
+
def output_diagram(diagram, io, options)
|
|
60
|
+
mermaid_syntax = render_mermaid(diagram, options)
|
|
61
|
+
io.puts mermaid_syntax
|
|
62
|
+
mermaid_syntax
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def transition_metadata_map
|
|
68
|
+
@last_transition_metadata_map ||= Array(@last_transition_metadata).each_with_object({}) do |data, memo|
|
|
69
|
+
transition = data[:transition]
|
|
70
|
+
memo[transition] = data if transition
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_mermaid(diagram, options)
|
|
75
|
+
return diagram.to_mermaid unless @last_transition_metadata
|
|
76
|
+
|
|
77
|
+
lines = ["stateDiagram-v2"]
|
|
78
|
+
|
|
79
|
+
diagram.states.each do |state|
|
|
80
|
+
fragment = state.respond_to?(:to_mermaid_fragment) ? state.to_mermaid_fragment : state.id
|
|
81
|
+
lines << " #{fragment}" if fragment && !fragment.empty?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
diagram.transitions.each do |transition|
|
|
85
|
+
metadata = transition_metadata_map[transition]
|
|
86
|
+
lines << " #{render_transition_line(transition, metadata, options)}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
lines.join("\n")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_transition_line(transition, metadata, options)
|
|
93
|
+
from_node = format_node_id(transition.source_state_id)
|
|
94
|
+
to_node = format_node_id(transition.target_state_id)
|
|
95
|
+
|
|
96
|
+
label_text = ''
|
|
97
|
+
|
|
98
|
+
base_label = transition.label
|
|
99
|
+
label_text = base_label if base_label && !base_label.empty?
|
|
100
|
+
|
|
101
|
+
guard_fragment = ''
|
|
102
|
+
if options[:show_conditions] && metadata
|
|
103
|
+
condition_tokens = build_condition_tokens(metadata[:conditions])
|
|
104
|
+
guard_fragment = "[#{condition_tokens.join(' && ')}]" if condition_tokens.any?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
action_fragment = ''
|
|
108
|
+
if options[:show_callbacks] && metadata
|
|
109
|
+
callback_tokens = build_callback_tokens(metadata[:callbacks])
|
|
110
|
+
action_fragment = "/ #{callback_tokens.join(', ')}" if callback_tokens.any?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
parts = []
|
|
114
|
+
parts << label_text unless label_text.empty?
|
|
115
|
+
parts << guard_fragment unless guard_fragment.empty?
|
|
116
|
+
parts << action_fragment unless action_fragment.empty?
|
|
117
|
+
label = parts.join(' ').strip
|
|
118
|
+
|
|
119
|
+
if label.empty?
|
|
120
|
+
"#{from_node} --> #{to_node}"
|
|
121
|
+
else
|
|
122
|
+
"#{from_node} --> #{to_node} : #{label}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_condition_tokens(conditions)
|
|
127
|
+
return [] unless conditions.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
tokens = []
|
|
130
|
+
Array(conditions[:if]).each do |token|
|
|
131
|
+
next if token.nil? || token.to_s.empty?
|
|
132
|
+
tokens << "if #{token}"
|
|
133
|
+
end
|
|
134
|
+
Array(conditions[:unless]).each do |token|
|
|
135
|
+
next if token.nil? || token.to_s.empty?
|
|
136
|
+
tokens << "unless #{token}"
|
|
137
|
+
end
|
|
138
|
+
tokens
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def build_callback_tokens(callbacks)
|
|
142
|
+
return [] unless callbacks.is_a?(Hash)
|
|
143
|
+
|
|
144
|
+
tokens = []
|
|
145
|
+
callbacks.each do |type, names|
|
|
146
|
+
Array(names).each do |name|
|
|
147
|
+
next if name.nil? || name.to_s.empty?
|
|
148
|
+
tokens << "#{type} #{format_callback_reference(name)}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
tokens
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def format_callback_reference(callback)
|
|
155
|
+
case callback
|
|
156
|
+
when Symbol, String
|
|
157
|
+
callback.to_s
|
|
158
|
+
when Proc, Method
|
|
159
|
+
if callback.respond_to?(:source_location) && callback.source_location
|
|
160
|
+
file, line = callback.source_location
|
|
161
|
+
filename = File.basename(file) if file
|
|
162
|
+
"lambda@#{filename}:#{line}"
|
|
163
|
+
else
|
|
164
|
+
'lambda'
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
callback.to_s
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def format_node_id(node_id)
|
|
172
|
+
node_id == '*' ? '[*]' : node_id
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def filter_mermaid_for_state(mermaid_syntax, state_name)
|
|
176
|
+
lines = mermaid_syntax.split("\n")
|
|
177
|
+
relevant_lines = lines.select do |line|
|
|
178
|
+
line.include?(state_name) || line.start_with?("stateDiagram")
|
|
179
|
+
end
|
|
180
|
+
relevant_lines.join("\n")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def filter_mermaid_for_event(mermaid_syntax, event_name)
|
|
184
|
+
lines = mermaid_syntax.split("\n")
|
|
185
|
+
relevant_lines = lines.select do |line|
|
|
186
|
+
line.include?(event_name) || line.start_with?("stateDiagram")
|
|
187
|
+
end
|
|
188
|
+
relevant_lines.join("\n")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'state_machines'
|
|
4
|
+
require 'state_machines-diagram'
|
|
5
|
+
require 'state_machines/mermaid/version'
|
|
6
|
+
require 'state_machines/mermaid/renderer'
|
|
7
|
+
|
|
8
|
+
# Set the renderer to use mermaid
|
|
9
|
+
StateMachines::Machine.renderer = StateMachines::Mermaid::Renderer
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
class MermaidRendererTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@dragon = Dragon.new
|
|
9
|
+
@mage = Mage.new
|
|
10
|
+
@troll = Troll.new
|
|
11
|
+
@regiment = Regiment.new
|
|
12
|
+
@battle = Battle.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_basic_mermaid_output
|
|
16
|
+
io = StringIO.new
|
|
17
|
+
StateMachines::Mermaid::Renderer.draw_machine(@dragon.class.state_machine(:mood), io: io)
|
|
18
|
+
|
|
19
|
+
output = io.string
|
|
20
|
+
assert_includes output, "stateDiagram-v2"
|
|
21
|
+
assert_includes output, "sleeping : sleeping"
|
|
22
|
+
assert_includes output, "sleeping --> hunting : wake_up"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_state_labels
|
|
26
|
+
io = StringIO.new
|
|
27
|
+
StateMachines::Mermaid::Renderer.draw_machine(@mage.class.state_machine(:concentration), io: io)
|
|
28
|
+
|
|
29
|
+
output = io.string
|
|
30
|
+
assert_includes output, "focused : focused"
|
|
31
|
+
assert_includes output, "deep_meditation : deep_meditation"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_complex_transitions_with_conditions
|
|
35
|
+
io = StringIO.new
|
|
36
|
+
StateMachines::Mermaid::Renderer.draw_machine(
|
|
37
|
+
Character.state_machine(:status),
|
|
38
|
+
io: io,
|
|
39
|
+
show_conditions: true
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
output = io.string
|
|
43
|
+
assert_includes output, "idle --> combat : engage"
|
|
44
|
+
assert_includes output, "[if can_fight?]"
|
|
45
|
+
assert_includes output, "[unless spell_locked?]"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_final_states
|
|
49
|
+
io = StringIO.new
|
|
50
|
+
StateMachines::Mermaid::Renderer.draw_machine(Character.state_machine(:status), io: io)
|
|
51
|
+
|
|
52
|
+
output = io.string
|
|
53
|
+
# The mermaid gem doesn't add [*] for final states by default
|
|
54
|
+
assert_includes output, "dead : dead"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_multiple_from_states
|
|
58
|
+
io = StringIO.new
|
|
59
|
+
StateMachines::Mermaid::Renderer.draw_machine(@troll.class.state_machine(:regeneration), io: io)
|
|
60
|
+
|
|
61
|
+
output = io.string
|
|
62
|
+
assert_includes output, "normal --> suppressed : take_fire_damage"
|
|
63
|
+
assert_includes output, "accelerated --> suppressed : take_fire_damage"
|
|
64
|
+
assert_includes output, "berserk --> suppressed : take_fire_damage"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_loopback_transitions
|
|
68
|
+
io = StringIO.new
|
|
69
|
+
StateMachines::Mermaid::Renderer.draw_machine(@dragon.class.state_machine(:mood), io: io)
|
|
70
|
+
|
|
71
|
+
output = io.string
|
|
72
|
+
assert_includes output, "hoarding --> hoarding : find_treasure"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_callbacks_option
|
|
76
|
+
io = StringIO.new
|
|
77
|
+
StateMachines::Mermaid::Renderer.draw_machine(
|
|
78
|
+
Character.state_machine(:status),
|
|
79
|
+
io: io,
|
|
80
|
+
show_callbacks: true
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
output = io.string
|
|
84
|
+
assert_includes output, " / before"
|
|
85
|
+
assert_includes output, "cast_spell"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_nested_states_in_spell_school
|
|
89
|
+
io = StringIO.new
|
|
90
|
+
StateMachines::Mermaid::Renderer.draw_machine(@mage.class.state_machine(:spell_school), io: io)
|
|
91
|
+
|
|
92
|
+
output = io.string
|
|
93
|
+
assert_includes output, "apprentice"
|
|
94
|
+
assert_includes output, "ember"
|
|
95
|
+
assert_includes output, "inferno"
|
|
96
|
+
assert_includes output, "archmage"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_battle_phase_machine
|
|
100
|
+
io = StringIO.new
|
|
101
|
+
StateMachines::Mermaid::Renderer.draw_machine(@battle.class.state_machine(:phase), io: io)
|
|
102
|
+
|
|
103
|
+
output = io.string
|
|
104
|
+
assert_includes output, "preparation"
|
|
105
|
+
assert_includes output, "deployment"
|
|
106
|
+
assert_includes output, "skirmish"
|
|
107
|
+
assert_includes output, "main_battle"
|
|
108
|
+
assert_includes output, "climax"
|
|
109
|
+
assert_includes output, "aftermath"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_regiment_formation_states
|
|
113
|
+
io = StringIO.new
|
|
114
|
+
StateMachines::Mermaid::Renderer.draw_machine(@regiment.class.state_machine(:formation), io: io)
|
|
115
|
+
|
|
116
|
+
output = io.string
|
|
117
|
+
assert_includes output, "column"
|
|
118
|
+
assert_includes output, "square"
|
|
119
|
+
assert_includes output, "wedge"
|
|
120
|
+
assert_includes output, "scattered"
|
|
121
|
+
assert_includes output, "column --> line : deploy"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_sanitizes_special_characters_in_ids
|
|
125
|
+
# Create a class with special characters in state names
|
|
126
|
+
klass = Class.new do
|
|
127
|
+
state_machine :status do
|
|
128
|
+
state :"waiting-for-input"
|
|
129
|
+
state :"processing/data"
|
|
130
|
+
state :"error!"
|
|
131
|
+
|
|
132
|
+
event :process do
|
|
133
|
+
transition :"waiting-for-input" => :"processing/data"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
event :fail do
|
|
137
|
+
transition :"processing/data" => :"error!"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
io = StringIO.new
|
|
143
|
+
StateMachines::Mermaid::Renderer.draw_machine(klass.state_machine(:status), io: io)
|
|
144
|
+
|
|
145
|
+
output = io.string
|
|
146
|
+
# The mermaid gem doesn't sanitize IDs by default
|
|
147
|
+
assert_includes output, "waiting-for-input"
|
|
148
|
+
assert_includes output, "processing/data"
|
|
149
|
+
assert_includes output, "error!"
|
|
150
|
+
assert_includes output, "waiting-for-input --> processing/data"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_dragon_age_progression
|
|
154
|
+
io = StringIO.new
|
|
155
|
+
StateMachines::Mermaid::Renderer.draw_machine(@dragon.class.state_machine(:age_category), io: io)
|
|
156
|
+
|
|
157
|
+
output = io.string
|
|
158
|
+
assert_includes output, "wyrmling --> young : age"
|
|
159
|
+
assert_includes output, "young --> adult : age"
|
|
160
|
+
assert_includes output, "adult --> ancient : age"
|
|
161
|
+
assert_includes output, "ancient --> great_wyrm : age"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_parallel_state_machines_dragon
|
|
165
|
+
mood_io = StringIO.new
|
|
166
|
+
flight_io = StringIO.new
|
|
167
|
+
age_io = StringIO.new
|
|
168
|
+
|
|
169
|
+
StateMachines::Mermaid::Renderer.draw_machine(@dragon.class.state_machine(:mood), io: mood_io)
|
|
170
|
+
StateMachines::Mermaid::Renderer.draw_machine(@dragon.class.state_machine(:flight), io: flight_io)
|
|
171
|
+
StateMachines::Mermaid::Renderer.draw_machine(@dragon.class.state_machine(:age_category), io: age_io)
|
|
172
|
+
|
|
173
|
+
# Each should generate valid mermaid syntax
|
|
174
|
+
[mood_io, flight_io, age_io].each do |io|
|
|
175
|
+
assert io.string.start_with?("stateDiagram-v2")
|
|
176
|
+
assert io.string.include?("-->")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def test_complex_conditions_formatting
|
|
181
|
+
io = StringIO.new
|
|
182
|
+
StateMachines::Mermaid::Renderer.draw_machine(@dragon.class.state_machine(:mood), io: io)
|
|
183
|
+
|
|
184
|
+
output = io.string
|
|
185
|
+
# Check that complex conditions are properly formatted
|
|
186
|
+
lines = output.split("\n")
|
|
187
|
+
transition_lines = lines.select { |l| l.include?("-->") && l.include?("[") }
|
|
188
|
+
|
|
189
|
+
# Check that at least some transitions exist
|
|
190
|
+
assert output.include?("-->"), "Should have transitions"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_all_character_types_render_properly
|
|
194
|
+
[@dragon, @mage, @troll, @regiment].each do |character|
|
|
195
|
+
character.class.state_machines.each do |name, machine|
|
|
196
|
+
io = StringIO.new
|
|
197
|
+
# Just call the method, any exception will fail the test
|
|
198
|
+
StateMachines::Mermaid::Renderer.draw_machine(machine, io: io)
|
|
199
|
+
|
|
200
|
+
output = io.string
|
|
201
|
+
assert output.start_with?("stateDiagram-v2"),
|
|
202
|
+
"#{character.class.name}##{name} should generate valid mermaid"
|
|
203
|
+
assert output.include?("-->"),
|
|
204
|
+
"#{character.class.name}##{name} should have transitions"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|