dm-is-state_machine 0.9.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1 +1,10 @@
1
+ === 2008-10-13
1
2
 
3
+ * Added functionality and docs
4
+
5
+ - Added support to pass a symbol to a transition, which will call the
6
+ corresponding method. This means you don't have to put a whole
7
+ Proc inside the state machine definition if you don't want to.
8
+ - Added support for the :exit transition.
9
+ - Created a new example called SlotMachine in /spec/integration
10
+ - Updated README and TODO.
data/Manifest.txt CHANGED
@@ -1,6 +1,7 @@
1
1
  History.txt
2
2
  LICENSE
3
3
  Manifest.txt
4
+ README.markdown
4
5
  README.txt
5
6
  Rakefile
6
7
  TODO
@@ -16,10 +17,12 @@ spec/examples/invalid_events.rb
16
17
  spec/examples/invalid_states.rb
17
18
  spec/examples/invalid_transitions_1.rb
18
19
  spec/examples/invalid_transitions_2.rb
20
+ spec/examples/slot_machine.rb
19
21
  spec/examples/traffic_light.rb
20
22
  spec/integration/invalid_events_spec.rb
21
23
  spec/integration/invalid_states_spec.rb
22
24
  spec/integration/invalid_transitions_spec.rb
25
+ spec/integration/slot_machine_spec.rb
23
26
  spec/integration/traffic_light_spec.rb
24
27
  spec/spec.opts
25
28
  spec/spec_helper.rb
data/README.markdown ADDED
@@ -0,0 +1,102 @@
1
+ # dm-is-state_machine #
2
+
3
+ DataMapper plugin that adds state machine functionality to your models.
4
+
5
+ ## Why is this plugin useful? ##
6
+
7
+ Your DataMapper resource might benefit from a state machine if it:
8
+
9
+ * has different "modes" of operation
10
+ * has discrete behaviors
11
+ * especially if the behaviors are mutually exclusive
12
+
13
+ And you want a clean, high-level way of describing these modes / behaviors
14
+ and how the resource moves between them. This plugin allows you to
15
+ declaratively describe the states and transitions involved.
16
+
17
+ ## Installation ##
18
+
19
+ 1. Download dm-more.
20
+ 2. Install dm-is-state_machine using the supplied rake files.
21
+
22
+ ## Setting up with Merb ##
23
+
24
+ Add this line to your init.rb:
25
+
26
+ dependency "dm-is-state_machine"
27
+
28
+ ## Example DataMapper resource (i.e. model) ##
29
+
30
+ # /app/models/traffic_light.rb
31
+ class TrafficLight
32
+ include DataMapper::Resource
33
+
34
+ property :id, Serial
35
+
36
+ is :state_machine, :initial => :green, :column => :color do
37
+ state :green
38
+ state :yellow
39
+ state :red, :enter => :red_hook
40
+ state :broken
41
+
42
+ event :forward do
43
+ transition :from => :green, :to => :yellow
44
+ transition :from => :yellow, :to => :red
45
+ transition :from => :red, :to => :green
46
+ end
47
+ end
48
+
49
+ def red_hook
50
+ # Do something
51
+ end
52
+ end
53
+
54
+ ## What this gives you ##
55
+
56
+ ### Explained in words ###
57
+
58
+ The above DSL (domain specific language) does these things "behind the scenes":
59
+
60
+ 1. Defines a DataMapper property called 'color'.
61
+
62
+ 2. Makes the current state available by using 'traffic_light.color'.
63
+
64
+ 3. Defines the 'forward!' transition method. This method triggers the
65
+ appropriate transition based on the current state and comparing it against
66
+ the various :from states. It will raise an error if you attempt to call
67
+ it with an invalid state (such as :broken, see above). After the method
68
+ runs successfully, the state machine will be left in the :to state.
69
+
70
+ ### Explained with some code examples ###
71
+
72
+ # Somewhere in your controller, perhaps
73
+ light = TrafficLight.new
74
+
75
+ # Move to the next state
76
+ light.forward!
77
+
78
+ # Do something based on the current state
79
+ case light.color
80
+ when "green"
81
+ # do something green-related
82
+ when "yellow"
83
+ # do something yellow-related
84
+ when "red"
85
+ # do something red-related
86
+ end
87
+
88
+ ## Specific examples ##
89
+
90
+ We would also like to hear how *you* are using state machines in your code.
91
+
92
+ ## See also ##
93
+
94
+ Here are some other projects you might want to look at. Most of them
95
+ are probably intended for ActiveRecord. They take different approaches,
96
+ which is pretty interesting. If you find something you like in these other
97
+ projects, let us know. Maybe we can incorporate some of your favorite parts.
98
+ That said, I do not want to create a Frankenstein. :)
99
+
100
+ * http://github.com/pluginaweek/state_machine/tree/master
101
+ * http://github.com/davidlee/stateful/tree/master
102
+ * http://github.com/sbfaulkner/has_states/tree/master
data/README.txt CHANGED
@@ -1,12 +1,5 @@
1
- = dm-state-machine
1
+ = dm-is-state_machine
2
2
 
3
- DataMapper plugin that adds state machine functionality.
3
+ == DESCRIPTION:
4
4
 
5
- == Installation
6
-
7
- Download dm-more and install dm-is-state_machine. Require it in your app.
8
-
9
- == Getting started
10
-
11
- Please refer to the integration specs in spec/integration, which refer to
12
- spec/examples.
5
+ See README.markdown
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ GEM_NAME = "dm-is-state_machine"
12
12
  GEM_VERSION = DataMapper::Is::StateMachine::VERSION
13
13
  GEM_DEPENDENCIES = [["dm-core", GEM_VERSION]]
14
14
  GEM_CLEAN = ["log", "pkg"]
15
- GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO ] }
15
+ GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt README.markdown LICENSE TODO ] }
16
16
 
17
17
  PROJECT_NAME = "datamapper"
18
18
  PROJECT_URL = "http://github.com/sam/dm-more/tree/master/dm-is-state_machine"
data/TODO CHANGED
@@ -1,11 +1,52 @@
1
1
  TODO
2
2
  ====
3
3
 
4
- * Should skipping to a new state automatically trigger :enter Proc?
5
- * Add loopback checking (i.e. when transitioning from a state back to itself)
6
- * Add support for callbacks
7
- * Consider using DataMapper's Enum type.
4
+ * Take a look at using DataMapper's Enum type.
5
+ - Does the Enum type support for all databases? (SQLite?)
6
+
7
+
8
+ * I am not real happy with spec/unit/dsl:
9
+ - The specs are brittle.
10
+ - The specs don't actually test much.
11
+
12
+ However, the integration specs are pretty thorough. Maybe each
13
+ serves their own purpose.
14
+
15
+
16
+ API QUESTIONS
17
+ =============
18
+
19
+ * Some people are asking for convenience methods.
20
+
21
+ For example, instead of:
22
+ light = TrafficLight.new
23
+ light.color == "yellow"
24
+
25
+ We could possibly offer:
26
+ light.yellow?
27
+
28
+ Advantages:
29
+ - More compact syntax.
30
+
31
+ Disadvantages:
32
+ - Namespace pollution
33
+ - Possible namespace collision if you have more than one state machine
34
+ in your model.
35
+
36
+
37
+ * Should skipping to a new state automatically trigger the :enter event?
38
+
39
+
40
+ * Should we add loopback checking?
41
+
42
+ In other words, if there is a transition defined from one state back to
43
+ the same state, should the :enter and :exit events fire?
44
+
45
+
46
+ * Should we add support for DataMapper callbacks? (e.g. after saving)
47
+
48
+
49
+ SOMEDAY / MAYBE
50
+ ===============
51
+
8
52
  * Consider trying out a nested state machine.
9
- * Not real happy with spec/unit/dsl:
10
- - specs are brittle
11
- - specs don't actually test much
@@ -3,7 +3,7 @@ require 'rubygems'
3
3
  require 'pathname'
4
4
 
5
5
  # Add all external dependencies for the plugin here
6
- gem 'dm-core', '=0.9.6'
6
+ gem 'dm-core', '~>0.9.7'
7
7
  require 'dm-core'
8
8
 
9
9
  # Require plugin-files
@@ -3,12 +3,29 @@ module DataMapper
3
3
  module StateMachine
4
4
  module Data
5
5
 
6
- # Represents one state machine
6
+ # This Machine class represents one state machine.
7
+ #
8
+ # A model (i.e. a DataMapper resource) can have more than one Machine.
7
9
  class Machine
8
10
 
9
- attr_reader :column, :initial
11
+ # The property of the DM resource that will hold this Machine's
12
+ # state.
13
+ #
14
+ # TODO: change :column to :property
15
+ attr_accessor :column
16
+
17
+ # The initial value of this Machine's state
18
+ attr_accessor :initial
19
+
20
+ # The current value of this Machine's state
21
+ #
22
+ # This is the "primary control" of this Machine's state. All
23
+ # other methods key off the value of @current_state_name.
10
24
  attr_accessor :current_state_name
11
- attr_accessor :events, :states
25
+
26
+ attr_accessor :events
27
+
28
+ attr_accessor :states
12
29
 
13
30
  def initialize(column, initial)
14
31
  @column, @initial = column, initial
@@ -27,14 +44,18 @@ module DataMapper
27
44
  t[:from].to_s == @current_state_name.to_s
28
45
  end
29
46
  unless transition
30
- raise InvalidEvent, "Event (#{event_name.inspect}) does " +
31
- "not exist for current state (#{@current_state_name.inspect})"
47
+ raise InvalidEvent, "Event (#{event_name.inspect}) does not" +
48
+ "exist for current state (#{@current_state_name.inspect})"
32
49
  end
50
+
51
+ # == Run :exit hook (if present) ==
52
+ resource.run_hook_if_present current_state.options[:exit]
53
+
54
+ # == Change the current_state ==
33
55
  @current_state_name = transition[:to]
34
56
 
35
- # ===== Call :enter Proc if present =====
36
- return unless enter_proc = current_state.options[:enter]
37
- enter_proc.call(resource)
57
+ # == Run :enter hook (if present) ==
58
+ resource.run_hook_if_present current_state.options[:enter]
38
59
  end
39
60
 
40
61
  # Return the current state
@@ -49,6 +49,17 @@ module DataMapper
49
49
  send(:"#{column}=", machine.current_state_name)
50
50
  end
51
51
 
52
+ # Possible alternative to the above:
53
+ # (class_eval is typically faster than define_method)
54
+ #
55
+ # self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
56
+ # def #{name}!
57
+ # machine.current_state_name = self.send(:"#{column}")
58
+ # machine.fire_event(name, self)
59
+ # self.send(:"#{column}="), machine.current_state_name
60
+ # end
61
+ # RUBY
62
+
52
63
  yield if block_given?
53
64
 
54
65
  # ===== Teardown context =====
@@ -39,13 +39,14 @@ module DataMapper
39
39
  @is_state_machine = { :machine => machine }
40
40
 
41
41
  # ===== Define callbacks =====
42
- before :save do
43
- if self.new_record?
44
- # ...
45
- else
46
- # ...
47
- end
48
- end
42
+ # TODO: define callbacks
43
+ # before :save do
44
+ # if self.new_record?
45
+ # # ...
46
+ # else
47
+ # # ...
48
+ # end
49
+ # end
49
50
 
50
51
  before :destroy do
51
52
  # Do we need to do anything here?
@@ -63,12 +64,12 @@ module DataMapper
63
64
  protected
64
65
 
65
66
  def push_state_machine_context(label)
66
- ((@is_state_machine ||= {})[:context] ||= []) << label
67
+ @is_state_machine ||= {}
68
+ @is_state_machine[:context] ||= []
69
+ @is_state_machine[:context] << label
67
70
 
68
- # Less DRY, though more readable to some
69
- # @is_state_machine ||= {}
70
- # @is_state_machine[:context] ||= []
71
- # @is_state_machine[:context] << label
71
+ # Compacted, but barely readable for humans
72
+ # ((@is_state_machine ||= {})[:context] ||= []) << label
72
73
  end
73
74
 
74
75
  def pop_state_machine_context
@@ -84,13 +85,22 @@ module DataMapper
84
85
 
85
86
  def initialize(*args)
86
87
  super
87
- # ===== Call :enter Proc if present =====
88
+ # ===== Run :enter hook if present =====
88
89
  return unless is_sm = self.class.instance_variable_get(:@is_state_machine)
89
90
  return unless machine = is_sm[:machine]
90
91
  return unless initial = machine.initial
91
92
  return unless initial_state = machine.find_state(initial)
92
- return unless enter_proc = initial_state.options[:enter]
93
- enter_proc.call(self)
93
+ run_hook_if_present initial_state.options[:enter]
94
+ end
95
+
96
+ # hook may be either a Proc or symbol
97
+ def run_hook_if_present(hook)
98
+ return unless hook
99
+ if hook.respond_to?(:call)
100
+ hook.call(self)
101
+ else
102
+ self.send(hook)
103
+ end
94
104
  end
95
105
 
96
106
  end # InstanceMethods
@@ -1,7 +1,7 @@
1
1
  module DataMapper
2
2
  module Is
3
3
  module StateMachine
4
- VERSION = "0.9.6"
4
+ VERSION = "0.9.7"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,48 @@
1
+ # A valid example
2
+ class SlotMachine
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+ property :power_on, Boolean, :default => false
7
+
8
+ is :state_machine, :initial => :off, :column => :mode do
9
+ state :off,
10
+ :enter => :power_down,
11
+ :exit => :power_up
12
+ state :idle
13
+ state :spinning
14
+ state :report_loss
15
+ state :report_win
16
+ state :pay_out
17
+
18
+ event :pull_crank do
19
+ transition :from => :idle, :to => :spinning
20
+ end
21
+
22
+ event :turn_off do
23
+ transition :from => :idle, :to => :off
24
+ end
25
+
26
+ event :turn_on do
27
+ transition :from => :off, :to => :idle
28
+ end
29
+ end
30
+
31
+ def initialize
32
+ @log = []
33
+ super
34
+ end
35
+
36
+ def power_up
37
+ self.power_on = true
38
+ @log << [:power_up, Time.now]
39
+ end
40
+
41
+ def power_down
42
+ self.power_on = false
43
+ @log << [:power_down, Time.now]
44
+ end
45
+
46
+ end
47
+
48
+ SlotMachine.auto_migrate!
@@ -1,4 +1,4 @@
1
- # A valid example of a resource with a state machine.
1
+ # A valid example
2
2
  class TrafficLight
3
3
  include DataMapper::Resource
4
4
 
@@ -0,0 +1,91 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+ require Pathname(__FILE__).dirname.expand_path.parent + 'examples/slot_machine'
4
+
5
+ describe SlotMachine do
6
+
7
+ before(:each) do
8
+ @sm = SlotMachine.new
9
+ end
10
+
11
+ it "should have an 'id' column" do
12
+ @sm.attributes.should have_key(:id)
13
+ end
14
+
15
+ it "should have a 'mode' column" do
16
+ @sm.attributes.should have_key(:mode)
17
+ end
18
+
19
+ it "should have a 'power_on' column" do
20
+ @sm.attributes.should have_key(:power_on)
21
+ end
22
+
23
+ it "should not have a 'state' column" do
24
+ @sm.attributes.should_not have_key(:state)
25
+ end
26
+
27
+ it "should start in the off state" do
28
+ @sm.mode.should == "off"
29
+ end
30
+
31
+ it "should start with power_on == false" do
32
+ @sm.power_on.should == false
33
+ end
34
+
35
+ describe "in :off mode" do
36
+
37
+ before(:each) do
38
+ @sm.mode = :off
39
+ @sm.power_on = false
40
+ end
41
+
42
+ it "should allow the mode to be set" do
43
+ @sm.mode = :idle
44
+ @sm.save
45
+ @sm.mode.should == "idle"
46
+ end
47
+
48
+ it "turn_on! should work from off mode" do
49
+ @sm.turn_on!
50
+ @sm.mode.should == "idle"
51
+ @sm.power_on.should == true
52
+ end
53
+
54
+ it "turn_on! should not work twice in a row" do
55
+ @sm.turn_on!
56
+ lambda {
57
+ @sm.turn_on!
58
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidEvent)
59
+ end
60
+
61
+ end
62
+
63
+ describe "in :idle mode" do
64
+
65
+ before(:each) do
66
+ @sm.mode = :idle
67
+ @sm.power_on = true
68
+ end
69
+
70
+ it "turn_on! should raise error" do
71
+ lambda {
72
+ @sm.turn_on!
73
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidEvent)
74
+ end
75
+
76
+ it "turn_off! should work" do
77
+ @sm.turn_off!
78
+ @sm.mode.should == "off"
79
+ @sm.power_on.should == false
80
+ end
81
+
82
+ it "turn_off! should not work twice in a row" do
83
+ @sm.turn_off!
84
+ lambda {
85
+ @sm.turn_off!
86
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidEvent)
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -9,15 +9,15 @@ describe TrafficLight do
9
9
  end
10
10
 
11
11
  it "should have an 'id' column" do
12
- @t.attributes.should include(:id)
12
+ @t.attributes.should have_key(:id)
13
13
  end
14
14
 
15
15
  it "should have a 'color' column" do
16
- @t.attributes.should include(:color)
16
+ @t.attributes.should have_key(:color)
17
17
  end
18
18
 
19
19
  it "should not have a 'state' column" do
20
- @t.attributes.should_not include(:state)
20
+ @t.attributes.should_not have_key(:state)
21
21
  end
22
22
 
23
23
  it "should start off in the green state" do
data/spec/spec_helper.rb CHANGED
@@ -10,7 +10,7 @@ def load_driver(name, default_uri)
10
10
  lib = "do_#{name}"
11
11
 
12
12
  begin
13
- gem lib, '>=0.9.5'
13
+ gem lib, '~>0.9.7'
14
14
  require lib
15
15
  DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
16
16
  DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
@@ -87,11 +87,17 @@ describe DataMapper::Is::StateMachine::Data::Machine do
87
87
  @machine.find_event(:turn_on).should == @turn_on
88
88
  end
89
89
 
90
- it "#fire_event should work" do
91
- @machine.fire_event(:turn_on, nil)
90
+ it "#fire_event should change state" do
91
+ resource = mock("resource")
92
+ resource.should_receive(:run_hook_if_present).exactly(2).times.with(nil)
93
+ @machine.fire_event(:turn_on, resource)
92
94
  @machine.current_state.should == @on_state
93
95
  @machine.current_state_name.should == :on
94
96
  end
97
+
95
98
  end
96
99
 
100
+ # TODO: spec fire_event where :run_hook_if_present fires two times,
101
+ # but with :enter the first and :exit the second.
102
+
97
103
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dm-is-state_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.6
4
+ version: 0.9.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - David James
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-10-12 00:00:00 -06:00
12
+ date: 2008-11-18 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -20,7 +20,7 @@ dependencies:
20
20
  requirements:
21
21
  - - "="
22
22
  - !ruby/object:Gem::Version
23
- version: 0.9.6
23
+ version: 0.9.7
24
24
  version:
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: hoe
@@ -30,7 +30,7 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.7.0
33
+ version: 1.8.2
34
34
  version:
35
35
  description: DataMapper plugin for creating state machines
36
36
  email:
@@ -41,12 +41,14 @@ extensions: []
41
41
 
42
42
  extra_rdoc_files:
43
43
  - README.txt
44
+ - README.markdown
44
45
  - LICENSE
45
46
  - TODO
46
47
  files:
47
48
  - History.txt
48
49
  - LICENSE
49
50
  - Manifest.txt
51
+ - README.markdown
50
52
  - README.txt
51
53
  - Rakefile
52
54
  - TODO
@@ -62,10 +64,12 @@ files:
62
64
  - spec/examples/invalid_states.rb
63
65
  - spec/examples/invalid_transitions_1.rb
64
66
  - spec/examples/invalid_transitions_2.rb
67
+ - spec/examples/slot_machine.rb
65
68
  - spec/examples/traffic_light.rb
66
69
  - spec/integration/invalid_events_spec.rb
67
70
  - spec/integration/invalid_states_spec.rb
68
71
  - spec/integration/invalid_transitions_spec.rb
72
+ - spec/integration/slot_machine_spec.rb
69
73
  - spec/integration/traffic_light_spec.rb
70
74
  - spec/spec.opts
71
75
  - spec/spec_helper.rb
@@ -98,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
102
  requirements: []
99
103
 
100
104
  rubyforge_project: datamapper
101
- rubygems_version: 1.2.0
105
+ rubygems_version: 1.3.1
102
106
  signing_key:
103
107
  specification_version: 2
104
108
  summary: DataMapper plugin for creating state machines