Skip to content

Commit 983169c

Browse files
committed
Use graphviz renderer hook
1 parent 7507969 commit 983169c

File tree

3 files changed

+43
-191
lines changed

3 files changed

+43
-191
lines changed

lib/state_machines-graphviz.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# frozen_string_literal: true
22

3-
require 'state_machines-diagram'
43
require 'state_machines/graphviz'
54
require 'state_machines/graphviz/renderer'
65

lib/state_machines/graphviz.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require 'state_machines'
4-
require 'state_machines-diagram'
54
require 'graphviz'
65
require 'state_machines/graphviz/graph'
76
require 'state_machines/graphviz/renderer'
Lines changed: 43 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,64 @@
11
# frozen_string_literal: true
22

3-
require 'state_machines/diagram/renderer'
4-
53
module StateMachines
64
module Graphviz
75
module Renderer
86
extend self
97

10-
VALID_OPTIONS = %i[
11-
name
12-
path
13-
format
14-
font
15-
orientation
16-
human_name
17-
human_names
18-
io
19-
show_conditions
20-
show_callbacks
21-
].freeze
22-
238
def draw_machine(machine, io: $stdout, **options)
24-
validate_options!(options)
25-
diagram, builder = StateMachines::Diagram::Renderer.build_state_diagram(machine, options)
26-
graph = build_graph(machine, diagram, builder, options)
9+
graph_options = options.dup
10+
name = graph_options.delete(:name) || "#{machine.owner_class.name}_#{machine.name}"
11+
12+
draw_options = { human_name: false }
13+
if graph_options.key?(:human_names)
14+
draw_options[:human_name] = graph_options.delete(:human_names)
15+
elsif graph_options.key?(:human_name)
16+
draw_options[:human_name] = graph_options.delete(:human_name)
17+
end
18+
19+
graph = Graph.new(name, graph_options)
20+
21+
machine.states.by_priority.each { |state| state.draw(graph, draw_options) }
22+
machine.events.each { |event| draw_event(event, graph, draw_options, io) }
23+
2724
graph.output
2825
graph
2926
end
3027

3128
def draw_state(state, graph, options = {}, io = $stdout)
32-
validate_options!(options)
33-
add_state_node(state, graph, options)
34-
graph
29+
node = graph.add_nodes(
30+
state.name ? state.name.to_s : 'nil',
31+
label: state.description(options),
32+
width: '1',
33+
height: '1',
34+
shape: state.final? ? 'doublecircle' : 'ellipse'
35+
)
36+
37+
graph.add_edges(graph.add_nodes('starting_state', shape: 'point'), node) if state.initial?
38+
true
3539
end
3640

3741
def draw_event(event, graph, options = {}, io = $stdout)
38-
validate_options!(options)
3942
machine = event.machine
4043
valid_states = machine.states.by_priority.map(&:name)
44+
event_label = options[:human_name] ? event.human_name(machine.owner_class) : event.name.to_s
4145

4246
event.branches.each do |branch|
43-
draw_branch(branch, graph, event, valid_states, io, options)
47+
draw_branch_with_label(branch, graph, event_label, machine, valid_states)
4448
end
4549

46-
graph
50+
true
4751
end
4852

49-
def draw_branch(branch, graph, event, valid_states, io = $stdout, options = {})
50-
validate_options!(options)
53+
def draw_branch(branch, graph, event, valid_states, io = $stdout)
5154
machine = event.machine
52-
event_label = human_names?(options) ? event.human_name(machine.owner_class) : event.name.to_s
55+
draw_branch_with_label(branch, graph, event.name.to_s, machine, valid_states)
56+
true
57+
end
58+
59+
private
5360

61+
def draw_branch_with_label(branch, graph, event_label, machine, valid_states)
5462
branch.state_requirements.each do |state_requirement|
5563
from_states = state_requirement[:from].filter(valid_states)
5664

@@ -67,180 +75,26 @@ def draw_branch(branch, graph, event, valid_states, io = $stdout, options = {})
6775
graph.add_edges(
6876
from_state,
6977
loopback ? from_state : to_state,
70-
edge_options(event_label, branch: branch, event: event, machine: machine, options: options)
78+
label: event_label,
79+
labelfontsize: 10,
80+
taillabel: callback_method_names(machine, branch, :before).join("\n"),
81+
headlabel: callback_method_names(machine, branch, :after).join("\n")
7182
)
7283
end
7384
end
74-
75-
graph
76-
end
77-
78-
private
79-
80-
def validate_options!(options)
81-
StateMachines::OptionsValidator.assert_valid_keys!(options, *VALID_OPTIONS)
82-
end
83-
84-
def build_graph(machine, diagram, builder, options)
85-
graph = StateMachines::Graph.new(graph_name(machine, options), graph_options(options))
86-
populate_graph(graph, machine, diagram, builder, options)
87-
graph
88-
end
89-
90-
def populate_graph(graph, machine, diagram, builder, options)
91-
state_lookup = build_state_lookup(machine)
92-
start_node = nil
93-
94-
diagram.states.each do |state_node|
95-
state = state_lookup[state_node.id]
96-
metadata = builder&.state_metadata&.[](state_node.id) || {}
97-
shape = metadata[:type] == 'final' ? 'doublecircle' : 'ellipse'
98-
label = state ? state.description(human_name: human_names?(options)) : (state_node.label || state_node.id)
99-
100-
node = graph.add_nodes(
101-
state_node.id,
102-
label: label,
103-
width: '1',
104-
height: '1',
105-
shape: shape
106-
)
107-
108-
if metadata[:type] == 'initial'
109-
start_node ||= graph.add_nodes('starting_state', shape: 'point')
110-
graph.add_edges(start_node, node)
111-
end
112-
end
113-
114-
transition_map = transition_metadata_map(builder)
115-
diagram.transitions.each do |transition|
116-
metadata = transition_map[transition]
117-
graph.add_edges(
118-
transition.source_state_id,
119-
transition.target_state_id,
120-
edge_options(transition.label, metadata: metadata, options: options)
121-
)
122-
end
123-
end
124-
125-
def build_state_lookup(machine)
126-
machine.states.each_with_object({}) do |state, memo|
127-
key = state.name ? state.name.to_s : 'nil_state'
128-
memo[key] = state
129-
end
130-
end
131-
132-
def graph_name(machine, options)
133-
options[:name] || "#{machine.owner_class.name}_#{machine.name}"
134-
end
135-
136-
def graph_options(options)
137-
options.slice(:path, :format, :font, :orientation)
13885
end
13986

140-
def transition_metadata_map(builder)
141-
Array(builder&.transition_metadata).each_with_object({}) do |metadata, memo|
142-
transition = metadata[:transition]
143-
memo[transition] = metadata if transition
144-
end
145-
end
146-
147-
def edge_options(label, metadata: nil, branch: nil, event: nil, machine: nil, options: {})
148-
edge_options = { labelfontsize: 10 }
149-
150-
label_text = label.to_s if label
151-
label_text = nil if label_text&.empty?
152-
153-
if options[:show_conditions] && metadata
154-
condition_tokens = build_condition_tokens(metadata[:conditions])
155-
unless condition_tokens.empty?
156-
guard_fragment = "[#{condition_tokens.join(' && ')}]"
157-
label_text = label_text ? "#{label_text} #{guard_fragment}" : guard_fragment
158-
end
159-
end
160-
161-
if options[:show_callbacks]
162-
callbacks = metadata ? metadata[:callbacks] : branch_callbacks(branch, event, machine)
163-
label_text = append_around_callbacks(label_text, callbacks)
164-
edge_options[:taillabel] = callback_label(callbacks, :before) if callback_label(callbacks, :before)
165-
edge_options[:headlabel] = callback_label(callbacks, :after) if callback_label(callbacks, :after)
166-
end
167-
168-
edge_options[:label] = label_text if label_text
169-
edge_options
170-
end
171-
172-
def build_condition_tokens(conditions)
173-
return [] unless conditions.is_a?(Hash)
174-
175-
tokens = []
176-
Array(conditions[:if]).each do |token|
177-
next if token.nil? || token.to_s.empty?
178-
tokens << "if #{token}"
179-
end
180-
Array(conditions[:unless]).each do |token|
181-
next if token.nil? || token.to_s.empty?
182-
tokens << "unless #{token}"
183-
end
184-
tokens
185-
end
186-
187-
def branch_callbacks(branch, event, machine)
188-
return {} unless branch && event && machine
189-
190-
{
191-
before: callback_method_names(machine, branch, event, :before),
192-
after: callback_method_names(machine, branch, event, :after),
193-
around: callback_method_names(machine, branch, event, :around)
194-
}
195-
end
196-
197-
def callback_method_names(machine, branch, event, type)
198-
machine.callbacks[type == :around ? :before : type].select do |callback|
87+
def callback_method_names(machine, branch, type)
88+
event_name = branch.event_requirement.values.first
89+
machine.callbacks[type].select do |callback|
19990
callback.branch.matches?(branch,
20091
from: branch.state_requirements.map { |req| req[:from] },
20192
to: branch.state_requirements.map { |req| req[:to] },
202-
on: event.name)
93+
on: event_name)
20394
end.flat_map do |callback|
20495
callback.instance_variable_get('@methods')
20596
end.compact
20697
end
207-
208-
def callback_label(callbacks, type)
209-
return nil unless callbacks.is_a?(Hash)
210-
211-
names = Array(callbacks[type]).compact
212-
return nil if names.empty?
213-
214-
names.join("\n")
215-
end
216-
217-
def append_around_callbacks(label_text, callbacks)
218-
return label_text unless callbacks.is_a?(Hash)
219-
220-
around = Array(callbacks[:around]).compact
221-
return label_text if around.empty?
222-
223-
around_fragment = "around #{around.join(', ')}"
224-
label_text ? "#{label_text} / #{around_fragment}" : around_fragment
225-
end
226-
227-
def add_state_node(state, graph, options)
228-
human_name = human_names?(options)
229-
node = graph.add_nodes(
230-
state.name ? state.name.to_s : 'nil',
231-
label: state.description(human_name: human_name),
232-
width: '1',
233-
height: '1',
234-
shape: state.final? ? 'doublecircle' : 'ellipse'
235-
)
236-
237-
graph.add_edges(graph.add_nodes('starting_state', shape: 'point'), node) if state.initial?
238-
node
239-
end
240-
241-
def human_names?(options)
242-
options[:human_name] || options[:human_names]
243-
end
24498
end
24599
end
246100
end

0 commit comments

Comments
 (0)