Skip to content

Commit

Permalink
Allow states / events to be drawn with their human name instead of th…
Browse files Browse the repository at this point in the history
…eir internal name. Closes #154
  • Loading branch information
obrie committed Feb 11, 2012
1 parent fb0259e commit 8c7eb74
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 12 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,7 @@
# master

* Allow states / events to be drawn with their human name instead of their internal name

## 1.1.2 / 2012-01-20

* Fix states not being initialized properly on ActiveRecord 3.2+
Expand Down
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -943,6 +943,12 @@ To generate multiple state machine graphs:
rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
```

To use human state / event names:

```bash
rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle HUMAN_NAMES=true
```

**Note** that this will generate a different file for every state machine defined
in the class. The generated files will use an output filename of the format
`#{class_name}_#{machine_name}.#{format}`.
Expand Down
8 changes: 6 additions & 2 deletions lib/state_machine/event.rb
Expand Up @@ -194,9 +194,13 @@ def reset
# configured.
#
# A collection of the generated edges will be returned.
def draw(graph)
#
# Configuration options:
# * <tt>:human_name</tt> - Whether to use the event's human name for the
# node's label that gets drawn on the graph
def draw(graph, options = {})
valid_states = machine.states.by_priority.map {|state| state.name}
branches.collect {|branch| branch.draw(graph, name, valid_states)}.flatten
branches.collect {|branch| branch.draw(graph, options[:human_name] ? human_name : name, valid_states)}.flatten
end

# Generates a nicely formatted description of this event's contents.
Expand Down
12 changes: 7 additions & 5 deletions lib/state_machine/machine.rb
Expand Up @@ -1863,16 +1863,18 @@ def within_transaction(object)
# Default is "Arial".
# * <tt>:orientation</tt> - The direction of the graph ("portrait" or
# "landscape"). Default is "portrait".
# * <tt>:output</tt> - Whether to generate the output of the graph
# * <tt>:human_names</tt> - Whether to use human state / event names for
# node labels on the graph instead of the internal name. Default is false.
def draw(options = {})
options = {
:name => "#{owner_class.name}_#{name}",
:path => '.',
:format => 'png',
:font => 'Arial',
:orientation => 'portrait'
:orientation => 'portrait',
:human_names => false
}.merge(options)
assert_valid_keys(options, :name, :path, :format, :font, :orientation)
assert_valid_keys(options, :name, :path, :format, :font, :orientation, :human_names)

begin
# Load the graphviz library
Expand All @@ -1884,13 +1886,13 @@ def draw(options = {})

# Add nodes
states.by_priority.each do |state|
node = state.draw(graph)
node = state.draw(graph, :human_name => options[:human_names])
node.fontname = options[:font]
end

# Add edges
events.each do |event|
edges = event.draw(graph)
edges = event.draw(graph, :human_name => options[:human_names])
edges.each {|edge| edge.fontname = options[:font]}
end

Expand Down
17 changes: 13 additions & 4 deletions lib/state_machine/state.rb
Expand Up @@ -125,8 +125,13 @@ def human_name(klass = @machine.owner_class)
# State.new(machine, :parked, :value => nil).description # => "parked (nil)"
# State.new(machine, :parked, :value => 1).description # => "parked (1)"
# State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
def description
description = name ? name.to_s : name.inspect
#
# Configuration options:
# * <tt>:human_name</tt> - Whether to use this state's human name in the
# description or just the internal name
def description(options = {})
label = options[:human_name] ? human_name : name
description = label ? label.to_s : label.inspect
description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
description
end
Expand Down Expand Up @@ -227,9 +232,13 @@ def call(object, method, method_missing = nil, *args, &block)
# state, then "doublecircle", otherwise "ellipse".
#
# The actual node generated on the graph will be returned.
def draw(graph)
#
# Configuration options:
# * <tt>:human_name</tt> - Whether to use the state's human name for the
# node's label that gets drawn on the graph
def draw(graph, options = {})
node = graph.add_node(name ? name.to_s : 'nil',
:label => description,
:label => description(options),
:width => '1',
:height => '1',
:shape => final? ? 'doublecircle' : 'ellipse'
Expand Down
3 changes: 2 additions & 1 deletion lib/tasks/state_machine.rb
@@ -1,5 +1,5 @@
namespace :state_machine do
desc 'Draws state machines using GraphViz (options: CLASS=User,Vehicle; FILE=user.rb,vehicle.rb [not required in Rails / Merb]; FONT=Arial; FORMAT=png; ORIENTATION=portrait'
desc 'Draws state machines using GraphViz (options: CLASS=User,Vehicle; FILE=user.rb,vehicle.rb [not required in Rails / Merb]; FONT=Arial; FORMAT=png; ORIENTATION=portrait; HUMAN_NAMES=true'
task :draw do
# Build drawing options
options = {}
Expand All @@ -8,6 +8,7 @@
options[:format] = ENV['FORMAT'] if ENV['FORMAT']
options[:font] = ENV['FONT'] if ENV['FONT']
options[:orientation] = ENV['ORIENTATION'] if ENV['ORIENTATION']
options[:human_names] = ENV['HUMAN_NAMES'] == 'true' if ENV['HUMAN_NAMES']

if defined?(Rails)
puts "Files are automatically loaded in Rails; ignoring FILE option" if options.delete(:file)
Expand Down
21 changes: 21 additions & 0 deletions test/unit/event_test.rb
Expand Up @@ -1021,6 +1021,27 @@ def test_should_use_event_name_for_edge_label
assert_equal 'park', @edges.first['label'].to_s.gsub('"', '')
end
end

class EventDrawingWithHumanNameTest < Test::Unit::TestCase
def setup
states = [:parked, :idling]

@machine = StateMachine::Machine.new(Class.new, :initial => :parked)
@machine.other_states(*states)

graph = GraphViz.new('G')
states.each {|state| graph.add_node(state.to_s)}

@machine.events << @event = StateMachine::Event.new(@machine , :park, :human_name => 'Park')
@event.transition :parked => :idling

@edges = @event.draw(graph, :human_name => true)
end

def test_should_use_event_human_name_for_edge_label
assert_equal 'Park', @edges.first['label'].to_s.gsub('"', '')
end
end
rescue LoadError
$stderr.puts 'Skipping GraphViz StateMachine::Event tests. `gem install ruby-graphviz` >= v0.9.0 and try again.'
end unless ENV['TRAVIS']
15 changes: 15 additions & 0 deletions test/unit/machine_test.rb
Expand Up @@ -3230,6 +3230,21 @@ def test_should_allow_orientation_to_be_portrait
assert_equal 'TB', graph['rankdir'].to_s.gsub('"', '')
end

if Constants::RGV_VERSION != '0.9.0'
def test_should_allow_human_names_to_be_displayed
@machine.event :ignite, :human_name => 'Ignite'
@machine.state :parked, :human_name => 'Parked'
@machine.state :idling, :human_name => 'Idling'
graph = @machine.draw(:human_names => true)

parked_node = graph.get_node('parked')
assert_equal 'Parked', parked_node['label'].to_s.gsub('"', '')

idling_node = graph.get_node('idling')
assert_equal 'Idling', idling_node['label'].to_s.gsub('"', '')
end
end

def teardown
FileUtils.rm Dir["{.,#{File.dirname(__FILE__)}}/*.{png,jpg}"]
end
Expand Down
41 changes: 41 additions & 0 deletions test/unit/state_test.rb
Expand Up @@ -115,6 +115,10 @@ def test_should_not_redefine_nil_predicate
def test_should_have_a_description
assert_equal 'nil', @state.description
end

def test_should_have_a_description_using_human_name
assert_equal 'nil', @state.description(:human_name => true)
end
end

class StateWithNameTest < Test::Unit::TestCase
Expand Down Expand Up @@ -149,6 +153,11 @@ def test_should_not_include_value_in_description
assert_equal 'parked', @state.description
end

def test_should_allow_using_human_name_in_description
@state.human_name = 'Parked'
assert_equal 'Parked', @state.description(:human_name => true)
end

def test_should_define_predicate
assert @klass.new.respond_to?(:parked?)
end
Expand Down Expand Up @@ -177,6 +186,11 @@ def test_should_have_a_description
assert_equal 'parked (nil)', @state.description
end

def test_should_have_a_description_with_human_name
@state.human_name = 'Parked'
assert_equal 'Parked (nil)', @state.description(:human_name => true)
end

def test_should_define_predicate
object = @klass.new
assert object.respond_to?(:parked?)
Expand All @@ -198,6 +212,11 @@ def test_should_not_include_value_in_description
assert_equal 'parked', @state.description
end

def test_should_allow_human_name_in_description
@state.human_name = 'Parked'
assert_equal 'Parked', @state.description(:human_name => true)
end

def test_should_match_symbolic_value
assert @state.matches?(:parked)
assert !@state.matches?('parked')
Expand All @@ -224,6 +243,11 @@ def test_should_include_value_in_description
assert_equal 'parked (1)', @state.description
end

def test_should_allow_human_name_in_description
@state.human_name = 'Parked'
assert_equal 'Parked (1)', @state.description(:human_name => true)
end

def test_should_match_integer_value
assert @state.matches?(1)
assert !@state.matches?(2)
Expand Down Expand Up @@ -956,6 +980,23 @@ def test_should_use_doublecircle_as_shape
assert_equal 'doublecircle', @node['shape'].to_s.gsub('"', '')
end
end

class StateDrawingWithHumanNameTest < Test::Unit::TestCase
def setup
@machine = StateMachine::Machine.new(Class.new)
@machine.states << @state = StateMachine::State.new(@machine, :parked, :human_name => 'Parked')
@machine.event :ignite do
transition :parked => :idling
end

graph = GraphViz.new('G')
@node = @state.draw(graph, :human_name => true)
end

def test_should_use_description_with_human_name_as_label
assert_equal 'Parked', @node['label'].to_s.gsub('"', '')
end
end
rescue LoadError
$stderr.puts 'Skipping GraphViz StateMachine::State tests. `gem install ruby-graphviz` >= v0.9.0 and try again.'
end unless ENV['TRAVIS']

0 comments on commit 8c7eb74

Please sign in to comment.