dm-is-state_machine 0.9.6 → 0.9.7

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.
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