11# frozen_string_literal: true
22
3- require 'state_machines/diagram/renderer'
4-
53module 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
246100end
0 commit comments