dm-is-state_machine 0.9.4

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 ADDED
@@ -0,0 +1 @@
1
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 David James
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,31 @@
1
+ History.txt
2
+ LICENSE
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ TODO
7
+ lib/dm-is-state_machine.rb
8
+ lib/dm-is-state_machine/is/data/event.rb
9
+ lib/dm-is-state_machine/is/data/machine.rb
10
+ lib/dm-is-state_machine/is/data/state.rb
11
+ lib/dm-is-state_machine/is/dsl/event_dsl.rb
12
+ lib/dm-is-state_machine/is/dsl/state_dsl.rb
13
+ lib/dm-is-state_machine/is/state_machine.rb
14
+ lib/dm-is-state_machine/is/version.rb
15
+ spec/examples/invalid_events.rb
16
+ spec/examples/invalid_states.rb
17
+ spec/examples/invalid_transitions_1.rb
18
+ spec/examples/invalid_transitions_2.rb
19
+ spec/examples/traffic_light.rb
20
+ spec/integration/invalid_events_spec.rb
21
+ spec/integration/invalid_states_spec.rb
22
+ spec/integration/invalid_transitions_spec.rb
23
+ spec/integration/traffic_light_spec.rb
24
+ spec/spec.opts
25
+ spec/spec_helper.rb
26
+ spec/unit/data/event_spec.rb
27
+ spec/unit/data/machine_spec.rb
28
+ spec/unit/data/state_spec.rb
29
+ spec/unit/dsl/event_dsl_spec.rb
30
+ spec/unit/dsl/state_dsl_spec.rb
31
+ spec/unit/state_machine_spec.rb
data/README.txt ADDED
@@ -0,0 +1,12 @@
1
+ = dm-state-machine
2
+
3
+ DataMapper plugin that adds state machine functionality.
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.
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'spec/rake/spectask'
4
+ require 'pathname'
5
+
6
+ ROOT = Pathname(__FILE__).dirname.expand_path
7
+ require ROOT + 'lib/dm-is-state_machine/is/version'
8
+
9
+ AUTHOR = "David James"
10
+ EMAIL = "djwonk [a] collectiveinsight [d] net"
11
+ GEM_NAME = "dm-is-state_machine"
12
+ GEM_VERSION = DataMapper::Is::StateMachine::VERSION
13
+ GEM_DEPENDENCIES = [["dm-core", GEM_VERSION]]
14
+ GEM_CLEAN = ["log", "pkg"]
15
+ GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.txt LICENSE TODO ] }
16
+
17
+ PROJECT_NAME = "datamapper"
18
+ PROJECT_URL = "http://github.com/sam/dm-more/tree/master/dm-is-state_machine"
19
+ PROJECT_DESCRIPTION = PROJECT_SUMMARY = "DataMapper plugin for creating state machines"
20
+
21
+ require ROOT.parent + 'tasks/hoe'
22
+
23
+ task :default => [ :spec ]
24
+
25
+ WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
26
+ SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
27
+
28
+ desc "Install #{GEM_NAME} #{GEM_VERSION}"
29
+ task :install => [ :package ] do
30
+ sh "#{SUDO} gem install --local pkg/#{GEM_NAME}-#{GEM_VERSION} --no-update-sources", :verbose => false
31
+ end
32
+
33
+ desc "Uninstall #{GEM_NAME} #{GEM_VERSION} (default ruby)"
34
+ task :uninstall => [ :clobber ] do
35
+ sh "#{SUDO} gem uninstall #{GEM_NAME} -v#{GEM_VERSION} -I -x", :verbose => false
36
+ end
37
+
38
+ desc 'Run specifications'
39
+ Spec::Rake::SpecTask.new(:spec) do |t|
40
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
41
+ t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
42
+
43
+ begin
44
+ t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
45
+ t.rcov_opts << '--exclude' << 'spec'
46
+ t.rcov_opts << '--text-summary'
47
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
48
+ rescue Exception
49
+ # rcov not installed
50
+ end
51
+ end
data/TODO ADDED
@@ -0,0 +1,11 @@
1
+ TODO
2
+ ====
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.
8
+ * 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
@@ -0,0 +1,27 @@
1
+ # Needed to import datamapper and other gems
2
+ require 'rubygems'
3
+ require 'pathname'
4
+
5
+ # Add all external dependencies for the plugin here
6
+ gem 'dm-core', '=0.9.4'
7
+ require 'dm-core'
8
+
9
+ # Require plugin-files
10
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-state_machine' / 'is' / 'state_machine'
11
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-state_machine' / 'is' / 'data' / 'event'
12
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-state_machine' / 'is' / 'data' / 'machine'
13
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-state_machine' / 'is' / 'data' / 'state'
14
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-state_machine' / 'is' / 'dsl' / 'event_dsl'
15
+ require Pathname(__FILE__).dirname.expand_path / 'dm-is-state_machine' / 'is' / 'dsl' / 'state_dsl'
16
+
17
+ # Include the plugin in Resource
18
+ module DataMapper
19
+ module Resource
20
+ module ClassMethods
21
+ include DataMapper::Is::StateMachine
22
+ end # module ClassMethods
23
+ end # module Resource
24
+ end # module DataMapper
25
+
26
+ # An alternative way to do the same thing as above:
27
+ # DataMapper::Model.append_extensions DataMapper::Is::StateMachine
@@ -0,0 +1,25 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ module Data
5
+
6
+ class Event
7
+
8
+ attr_reader :name, :machine, :transitions
9
+
10
+ def initialize(name, machine)
11
+ @name = name
12
+ @machine = machine
13
+ @transitions = []
14
+ end
15
+
16
+ def add_transition(from, to)
17
+ @transitions << { :from => from, :to => to }
18
+ end
19
+
20
+ end
21
+
22
+ end # Data
23
+ end # StateMachine
24
+ end # Is
25
+ end # DataMapper
@@ -0,0 +1,69 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ module Data
5
+
6
+ # Represents one state machine
7
+ class Machine
8
+
9
+ attr_reader :column, :initial
10
+ attr_accessor :current_state_name
11
+ attr_accessor :events, :states
12
+
13
+ def initialize(column, initial)
14
+ @column, @initial = column, initial
15
+ @events, @states = [], []
16
+ @current_state_name = initial
17
+ end
18
+
19
+ # Fire (activate) the event with name +event_name+
20
+ #
21
+ # @api public
22
+ def fire_event(event_name, resource)
23
+ unless event = find_event(event_name)
24
+ raise InvalidEvent, "Could not find event (#{event_name.inspect})"
25
+ end
26
+ transition = event.transitions.find do |t|
27
+ t[:from].to_s == @current_state_name.to_s
28
+ end
29
+ unless transition
30
+ raise InvalidEvent, "Event (#{event_name.inspect}) does " +
31
+ "not exist for current state (#{@current_state_name.inspect})"
32
+ end
33
+ @current_state_name = transition[:to]
34
+
35
+ # ===== Call :enter Proc if present =====
36
+ return unless enter_proc = current_state.options[:enter]
37
+ enter_proc.call(resource)
38
+ end
39
+
40
+ # Return the current state
41
+ #
42
+ # @api public
43
+ def current_state
44
+ find_state(@current_state_name)
45
+ # TODO: add caching, i.e. with `@current_state ||= ...`
46
+ end
47
+
48
+ # Find event whose name is +event_name+
49
+ #
50
+ # @api semipublic
51
+ def find_event(event_name)
52
+ @events.find { |event| event.name.to_s == event_name.to_s }
53
+ # TODO: use a data structure that prevents duplicates
54
+ end
55
+
56
+ # Find state whose name is +event_name+
57
+ #
58
+ # @api semipublic
59
+ def find_state(state_name)
60
+ @states.find { |state| state.name.to_s == state_name.to_s }
61
+ # TODO: use a data structure that prevents duplicates
62
+ end
63
+
64
+ end
65
+
66
+ end # Data
67
+ end # StateMachine
68
+ end # Is
69
+ end # DataMapper
@@ -0,0 +1,21 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ module Data
5
+
6
+ class State
7
+
8
+ attr_reader :name, :machine, :options
9
+
10
+ def initialize(name, machine, options = {})
11
+ @name = name
12
+ @options = options
13
+ @machine = machine
14
+ end
15
+
16
+ end
17
+
18
+ end # Data
19
+ end # StateMachine
20
+ end # Is
21
+ end # DataMapper
@@ -0,0 +1,73 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ # Event DSL (Domain Specific Language)
5
+ module EventDsl
6
+
7
+ # Define an event. This takes a block which describes all valid
8
+ # transitions for this event.
9
+ #
10
+ # Example:
11
+ #
12
+ # class TrafficLight
13
+ # include DataMapper::Resource
14
+ # property :id, Serial
15
+ # is :state_machine, :initial => :green, :column => :color do
16
+ # # state definitions go here...
17
+ #
18
+ # event :forward do
19
+ # transition :from => :green, :to => :yellow
20
+ # transition :from => :yellow, :to => :red
21
+ # transition :from => :red, :to => :green
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # +transition+ takes a hash where <tt>:to</tt> is the state to transition
27
+ # to and <tt>:from</tt> is a state (or Array of states) from which this
28
+ # event can be fired.
29
+ def event(name, &block)
30
+ unless state_machine_context?(:is)
31
+ raise InvalidContext, "Valid only in 'is :state_machine' block"
32
+ end
33
+
34
+ # ===== Setup context =====
35
+ machine = @is_state_machine[:machine]
36
+ event = Data::Event.new(name, machine)
37
+ machine.events << event
38
+ @is_state_machine[:event] = {
39
+ :name => name,
40
+ :object => event
41
+ }
42
+ push_state_machine_context(:event)
43
+
44
+ # ===== Define methods =====
45
+ column = machine.column
46
+ define_method("#{name}!") do
47
+ machine.current_state_name = send(:"#{column}")
48
+ machine.fire_event(name, self)
49
+ send(:"#{column}=", machine.current_state_name)
50
+ end
51
+
52
+ yield if block_given?
53
+
54
+ # ===== Teardown context =====
55
+ pop_state_machine_context
56
+ end
57
+
58
+ def transition(options)
59
+ unless state_machine_context?(:event)
60
+ raise InvalidContext, "Valid only in 'event' block"
61
+ end
62
+ event_name = @is_state_machine[:event][:name]
63
+ event_object = @is_state_machine[:event][:object]
64
+
65
+ from = options[:from]
66
+ to = options[:to]
67
+ event_object.add_transition(from, to)
68
+ end
69
+
70
+ end # EventDsl
71
+ end # StateMachine
72
+ end # Is
73
+ end # DataMapper
@@ -0,0 +1,40 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ # State DSL (Domain Specific Language)
5
+ module StateDsl
6
+
7
+ # Define a state of the system.
8
+ #
9
+ # Example:
10
+ #
11
+ # class TrafficLight
12
+ # include DataMapper::Resource
13
+ # property :id, Serial
14
+ # is :state_machine do
15
+ # state :green, :enter => Proc.new { |o| o.log("G") }
16
+ # state :yellow, :enter => Proc.new { |o| o.log("Y") }
17
+ # state :red, :enter => Proc.new { |o| o.log("R") }
18
+ #
19
+ # # event definitions go here...
20
+ # end
21
+ #
22
+ # def log(string)
23
+ # Merb::Logger.info(string)
24
+ # end
25
+ # end
26
+ def state(name, options = {})
27
+ unless state_machine_context?(:is)
28
+ raise InvalidContext, "Valid only in 'is :state_machine' block"
29
+ end
30
+
31
+ # ===== Setup context =====
32
+ machine = @is_state_machine[:machine]
33
+ state = Data::State.new(name, machine, options)
34
+ machine.states << state
35
+ end
36
+
37
+ end # StateDsl
38
+ end # StateMachine
39
+ end # Is
40
+ end # DataMapper
@@ -0,0 +1,107 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+
5
+ class InvalidContext < RuntimeError; end
6
+ class InvalidState < RuntimeError; end
7
+ class InvalidEvent < RuntimeError; end
8
+ class EventConfusion < RuntimeError; end
9
+ class DuplicateStates < RuntimeError; end
10
+ class NoInitialState < RuntimeError; end
11
+
12
+ ##
13
+ # Makes a column ('state' by default) act as a state machine. It will
14
+ # define the property if it does not exist.
15
+ #
16
+ # @example [Usage]
17
+ # is :state_machine
18
+ # is :state_machine, :initial => :internal
19
+ # is :state_machine, :column => :availability
20
+ # is :state_machine, :column => :availability, :initial => :external
21
+ #
22
+ # @param options<Hash> a hash of options
23
+ #
24
+ # @option :column<Symbol> the name of the custom column
25
+ #
26
+ def is_state_machine(options = {}, &block)
27
+ extend DataMapper::Is::StateMachine::EventDsl
28
+ extend DataMapper::Is::StateMachine::StateDsl
29
+ include DataMapper::Is::StateMachine::InstanceMethods
30
+
31
+ # ===== Setup context =====
32
+ options = { :column => :state, :initial => nil }.merge(options)
33
+ column = options[:column]
34
+ initial = options[:initial].to_s
35
+ unless properties.detect { |p| p.name == column }
36
+ property column, String, :default => initial
37
+ end
38
+ machine = Data::Machine.new(column, initial)
39
+ @is_state_machine = { :machine => machine }
40
+
41
+ # ===== Define callbacks =====
42
+ before :save do
43
+ if self.new_record?
44
+ # ...
45
+ else
46
+ # ...
47
+ end
48
+ end
49
+
50
+ before :destroy do
51
+ # Do we need to do anything here?
52
+ end
53
+
54
+ # ===== Setup context =====
55
+ push_state_machine_context(:is)
56
+
57
+ yield if block_given?
58
+
59
+ # ===== Teardown context =====
60
+ pop_state_machine_context
61
+ end
62
+
63
+ protected
64
+
65
+ def push_state_machine_context(label)
66
+ ((@is_state_machine ||= {})[:context] ||= []) << label
67
+
68
+ # Less DRY, though more readable to some
69
+ # @is_state_machine ||= {}
70
+ # @is_state_machine[:context] ||= []
71
+ # @is_state_machine[:context] << label
72
+ end
73
+
74
+ def pop_state_machine_context
75
+ @is_state_machine[:context].pop
76
+ end
77
+
78
+ def state_machine_context?(label)
79
+ (i = @is_state_machine) && (c = i[:context]) &&
80
+ c.respond_to?(:include?) && c.include?(label)
81
+ end
82
+
83
+ module InstanceMethods
84
+
85
+ def initialize(*args)
86
+ super
87
+ # ===== Call :enter Proc if present =====
88
+ return unless is_sm = self.class.instance_variable_get(:@is_state_machine)
89
+ return unless machine = is_sm[:machine]
90
+ return unless initial = machine.initial
91
+ return unless initial_state = machine.find_state(initial)
92
+ return unless enter_proc = initial_state.options[:enter]
93
+ enter_proc.call(self)
94
+ end
95
+
96
+ end # InstanceMethods
97
+
98
+ end # StateMachine
99
+ end # Is
100
+ end # DataMapper
101
+
102
+ # Notes
103
+ # -----
104
+ #
105
+ # Since this gets mixed into a class, I try to keep the namespace pollution
106
+ # down to a minimum. This is why I only use the @is_state_machine instance
107
+ # variable.
@@ -0,0 +1,7 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ VERSION = "0.9.4"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # An invalid example.
2
+ class InvalidEvents
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+
7
+ is :state_machine do
8
+ state :day
9
+ state :night
10
+ end
11
+
12
+ # The next lines are intentionally incorrect.
13
+ #
14
+ # 'event' only makes sense in a block under 'is :state_machine'
15
+ event :sunrise
16
+ event :sunset
17
+
18
+ end
19
+
20
+ InvalidEvents.auto_migrate!
@@ -0,0 +1,20 @@
1
+ # An invalid example.
2
+ class InvalidStates
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+
7
+ is :state_machine do
8
+ event :sunrise
9
+ event :sunset
10
+ end
11
+
12
+ # The next lines are intentionally incorrect.
13
+ #
14
+ # 'state' only makes sense in a block under 'is :state_machine'
15
+ state :light
16
+ state :dark
17
+
18
+ end
19
+
20
+ InvalidStates.auto_migrate!
@@ -0,0 +1,22 @@
1
+ # An invalid example.
2
+ class InvalidTransitions1
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+
7
+ is :state_machine do
8
+ state :happy
9
+ state :sad
10
+
11
+ event :toggle
12
+
13
+ # The next lines are intentionally incorrect.
14
+ #
15
+ # 'transition' is only valid when nested beneath 'event'
16
+ transition :to => :happy, :from => :sad
17
+ transition :to => :sad, :from => :happy
18
+ end
19
+
20
+ end
21
+
22
+ InvalidTransitions1.auto_migrate!
@@ -0,0 +1,22 @@
1
+ # An invalid example.
2
+ class InvalidTransitions2
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+
7
+ is :state_machine do
8
+ state :happy
9
+ state :sad
10
+
11
+ event :toggle
12
+ end
13
+
14
+ # The next lines are intentionally incorrect.
15
+ #
16
+ # 'transition' is only valid when nested beneath 'event'
17
+ transition :to => :happy, :from => :sad
18
+ transition :to => :sad, :from => :happy
19
+
20
+ end
21
+
22
+ InvalidTransitions2.auto_migrate!
@@ -0,0 +1,44 @@
1
+ # A valid example of a resource with a state machine.
2
+ class TrafficLight
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial # see note 1
6
+
7
+ is :state_machine, :initial => :green, :column => :color do
8
+ state :green, :enter => Proc.new { |o| o.log << "G" }
9
+ state :yellow, :enter => Proc.new { |o| o.log << "Y" }
10
+ state :red, :enter => Proc.new { |o| o.log << "R" }
11
+
12
+ event :forward do
13
+ transition :from => :green, :to => :yellow
14
+ transition :from => :yellow, :to => :red
15
+ transition :from => :red, :to => :green
16
+ end
17
+
18
+ event :backward do
19
+ transition :from => :green, :to => :red
20
+ transition :from => :yellow, :to => :green
21
+ transition :from => :red, :to => :yellow
22
+ end
23
+ end
24
+
25
+ def log; @log ||= [] end
26
+
27
+ attr_reader :init
28
+ def initialize(*args)
29
+ (@init ||= []) << :init
30
+ super
31
+ end
32
+
33
+ end
34
+
35
+ TrafficLight.auto_migrate!
36
+
37
+ # ===== Note 1 =====
38
+ #
39
+ # One would expect that these two would be the same:
40
+ # property :id, Serial
41
+ # property :id, Integer, :serial => true
42
+ #
43
+ # But on 2008-07-05, the 2nd led to problems with an in-memory SQLite
44
+ # database.
@@ -0,0 +1,12 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ describe "InvalidEvents" do
5
+
6
+ it "should get InvalidContext when requiring" do
7
+ lambda {
8
+ require File.join( File.dirname(__FILE__), "..", "examples", "invalid_events" )
9
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
10
+ end
11
+
12
+ end
@@ -0,0 +1,12 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ describe "InvalidStates" do
5
+
6
+ it "should get InvalidContext when requiring" do
7
+ lambda {
8
+ require File.join( File.dirname(__FILE__), "..", "examples", "invalid_states" )
9
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
10
+ end
11
+
12
+ end
@@ -0,0 +1,22 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ describe "InvalidTransitions1" do
5
+
6
+ it "should get InvalidContext when requiring" do
7
+ lambda {
8
+ require File.join( File.dirname(__FILE__), "..", "examples", "invalid_transitions_1" )
9
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
10
+ end
11
+
12
+ end
13
+
14
+ describe "InvalidTransitions2" do
15
+
16
+ it "should get InvalidContext when requiring" do
17
+ lambda {
18
+ require File.join( File.dirname(__FILE__), "..", "examples", "invalid_transitions_2" )
19
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidContext)
20
+ end
21
+
22
+ end
@@ -0,0 +1,150 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+ require Pathname(__FILE__).dirname.expand_path.parent + 'examples/traffic_light'
4
+
5
+ describe TrafficLight do
6
+
7
+ before(:each) do
8
+ @t = TrafficLight.new
9
+ end
10
+
11
+ it "should have an 'id' column" do
12
+ @t.attributes.should include(:id)
13
+ end
14
+
15
+ it "should have a 'color' column" do
16
+ @t.attributes.should include(:color)
17
+ end
18
+
19
+ it "should not have a 'state' column" do
20
+ @t.attributes.should_not include(:state)
21
+ end
22
+
23
+ it "should start off in the green state" do
24
+ @t.color.should == "green"
25
+ end
26
+
27
+ it "should allow the color to be set" do
28
+ @t.color = :yellow
29
+ @t.save
30
+ @t.color.should == "yellow"
31
+ end
32
+
33
+ it "should have called the :enter Proc" do
34
+ @t.log.should == %w(G)
35
+ end
36
+
37
+ it "should call the original initialize method" do
38
+ @t.init.should == [:init]
39
+ end
40
+
41
+ describe 'forward!' do
42
+
43
+ it "should respond to :forward!" do
44
+ @t.respond_to?(:forward!).should == true
45
+ end
46
+
47
+ it "should transition to :yellow, :red, :green" do
48
+ @t.color.should == "green"
49
+ @t.forward!
50
+ @t.color.should == "yellow"
51
+ @t.log.should == %w(G Y)
52
+ @t.forward!
53
+ @t.color.should == "red"
54
+ @t.log.should == %w(G Y R)
55
+ @t.forward!
56
+ @t.color.should == "green"
57
+ @t.log.should == %w(G Y R G)
58
+ @t.new_record?.should == true
59
+ end
60
+
61
+ it "should skip to :yellow then transition to :red, :green, :yellow" do
62
+ @t.color = :yellow
63
+ @t.color.should == "yellow"
64
+ @t.log.should == %w(G)
65
+ @t.forward!
66
+ @t.color.should == "red"
67
+ @t.log.should == %w(G R)
68
+ @t.forward!
69
+ @t.color.should == "green"
70
+ @t.log.should == %w(G R G)
71
+ @t.forward!
72
+ @t.color.should == "yellow"
73
+ @t.log.should == %w(G R G Y)
74
+ @t.new_record?.should == true
75
+ end
76
+
77
+ it "should skip to :red then transition to :green, :yellow, :red" do
78
+ @t.color = :red
79
+ @t.color.should == "red"
80
+ @t.log.should == %w(G)
81
+ @t.forward!
82
+ @t.color.should == "green"
83
+ @t.log.should == %w(G G)
84
+ @t.forward!
85
+ @t.color.should == "yellow"
86
+ @t.log.should == %w(G G Y)
87
+ @t.forward!
88
+ @t.color.should == "red"
89
+ @t.log.should == %w(G G Y R)
90
+ @t.new_record?.should == true
91
+ end
92
+
93
+ end
94
+
95
+ describe 'backward!' do
96
+
97
+ it "should respond to 'backward!'" do
98
+ @t.respond_to?(:backward!).should == true
99
+ end
100
+
101
+ it "should transition to :red, :yellow, :green" do
102
+ @t.color.should == "green"
103
+ @t.log.should == %w(G)
104
+ @t.backward!
105
+ @t.color.should == "red"
106
+ @t.log.should == %w(G R)
107
+ @t.backward!
108
+ @t.color.should == "yellow"
109
+ @t.log.should == %w(G R Y)
110
+ @t.backward!
111
+ @t.color.should == "green"
112
+ @t.log.should == %w(G R Y G)
113
+ @t.new_record?.should == true
114
+ end
115
+
116
+ it "should skip to :yellow then transition to :green, :red, :yellow" do
117
+ @t.color = :yellow
118
+ @t.color.should == "yellow"
119
+ @t.log.should == %w(G)
120
+ @t.backward!
121
+ @t.color.should == "green"
122
+ @t.log.should == %w(G G)
123
+ @t.backward!
124
+ @t.color.should == "red"
125
+ @t.log.should == %w(G G R)
126
+ @t.backward!
127
+ @t.color.should == "yellow"
128
+ @t.log.should == %w(G G R Y)
129
+ @t.new_record?.should == true
130
+ end
131
+
132
+ it "should skip to :red then transition to :yellow, :green, :red" do
133
+ @t.color = :red
134
+ @t.color.should == "red"
135
+ @t.log.should == %w(G)
136
+ @t.backward!
137
+ @t.color.should == "yellow"
138
+ @t.log.should == %w(G Y)
139
+ @t.backward!
140
+ @t.color.should == "green"
141
+ @t.log.should == %w(G Y G)
142
+ @t.backward!
143
+ @t.color.should == "red"
144
+ @t.log.should == %w(G Y G R)
145
+ @t.new_record?.should == true
146
+ end
147
+
148
+ end
149
+
150
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --format specdoc
2
+ --colour
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ gem 'rspec', '>=1.1.3'
3
+ require 'spec'
4
+ require 'pathname'
5
+ require Pathname(__FILE__).dirname.expand_path.parent + 'lib/dm-is-state_machine'
6
+
7
+ def load_driver(name, default_uri)
8
+ return false if ENV['ADAPTER'] != name.to_s
9
+
10
+ lib = "do_#{name}"
11
+
12
+ begin
13
+ gem lib, '=0.9.4'
14
+ require lib
15
+ DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
16
+ DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
17
+ true
18
+ rescue Gem::LoadError => e
19
+ warn "Could not load #{lib}: #{e}"
20
+ false
21
+ end
22
+ end
23
+
24
+ ENV['ADAPTER'] ||= 'sqlite3'
25
+
26
+ HAS_SQLITE3 = load_driver(:sqlite3, 'sqlite3::memory:')
27
+ HAS_MYSQL = load_driver(:mysql, 'mysql://localhost/dm_core_test')
28
+ HAS_POSTGRES = load_driver(:postgres, 'postgres://postgres@localhost/dm_core_test')
@@ -0,0 +1,28 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent.parent + 'spec_helper'
3
+
4
+ module EventHelper
5
+ def new_event(*args)
6
+ DataMapper::Is::StateMachine::Data::Event.new(*args)
7
+ end
8
+ end
9
+
10
+ describe DataMapper::Is::StateMachine::Data::Event do
11
+ include EventHelper
12
+
13
+ before(:each) do
14
+ @machine = mock("machine")
15
+ @event = new_event(:ping, @machine)
16
+ end
17
+
18
+ it "#initialize should work" do
19
+ @event.name.should == :ping
20
+ @event.machine.should == @machine
21
+ @event.transitions.should == []
22
+ end
23
+
24
+ it "#add_transition should work" do
25
+ @event.add_transition(:nothing, :pinged)
26
+ @event.transitions.should == [{:from => :nothing, :to => :pinged }]
27
+ end
28
+ end
@@ -0,0 +1,97 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent.parent + 'spec_helper'
3
+
4
+ module MachineHelper
5
+ def new_machine(*args)
6
+ DataMapper::Is::StateMachine::Data::Machine.new(*args)
7
+ end
8
+
9
+ def new_state(name, machine, options = {})
10
+ mock(name, :name => name, :machine => machine, :options => options)
11
+ end
12
+
13
+ def new_event(name, machine)
14
+ mock(name, :name => name, :machine => machine)
15
+ end
16
+ end
17
+
18
+ describe DataMapper::Is::StateMachine::Data::Machine do
19
+ include MachineHelper
20
+
21
+ describe "new Machine, no events" do
22
+ before(:each) do
23
+ @machine = new_machine(:power, :off)
24
+ end
25
+
26
+ it "#column should work" do
27
+ @machine.column.should == :power
28
+ end
29
+
30
+ it "#initial should work" do
31
+ @machine.initial.should == :off
32
+ end
33
+
34
+ it "#events should work" do
35
+ @machine.events.should == []
36
+ end
37
+
38
+ it "#states should work" do
39
+ @machine.states.should == []
40
+ end
41
+
42
+ it "#find_event should return nothing" do
43
+ @machine.find_event(:turn_on).should == nil
44
+ end
45
+
46
+ it "#fire_event should raise error" do
47
+ lambda {
48
+ @machine.fire_event(:turn_on, nil)
49
+ }.should raise_error(DataMapper::Is::StateMachine::InvalidEvent)
50
+ end
51
+ end
52
+
53
+ describe "new Machine, 2 states, 1 event" do
54
+ before(:each) do
55
+ @machine = new_machine(:power, :off)
56
+ @machine.states << (@off_state = new_state(:off, @machine))
57
+ @machine.states << (@on_state = new_state(:on, @machine))
58
+ @machine.events << (@turn_on = new_event(:turn_on, @machine))
59
+ @turn_on.stub!(:transitions).and_return([{ :from => :off, :to => :on }])
60
+ end
61
+
62
+ it "#column should work" do
63
+ @machine.column.should == :power
64
+ end
65
+
66
+ it "#initial should work" do
67
+ @machine.initial.should == :off
68
+ end
69
+
70
+ it "#events should work" do
71
+ @machine.events.should == [@turn_on]
72
+ end
73
+
74
+ it "#states should work" do
75
+ @machine.states.should == [@off_state, @on_state]
76
+ end
77
+
78
+ it "#current_state should work" do
79
+ @machine.current_state.should == @off_state
80
+ end
81
+
82
+ it "#current_state_name should work" do
83
+ @machine.current_state_name.should == :off
84
+ end
85
+
86
+ it "#find_event should return nothing" do
87
+ @machine.find_event(:turn_on).should == @turn_on
88
+ end
89
+
90
+ it "#fire_event should work" do
91
+ @machine.fire_event(:turn_on, nil)
92
+ @machine.current_state.should == @on_state
93
+ @machine.current_state_name.should == :on
94
+ end
95
+ end
96
+
97
+ end
@@ -0,0 +1,22 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent.parent + 'spec_helper'
3
+
4
+ module StateHelper
5
+ def new_state(*args)
6
+ DataMapper::Is::StateMachine::Data::State.new(*args)
7
+ end
8
+ end
9
+
10
+ describe DataMapper::Is::StateMachine::Data::State do
11
+ include StateHelper
12
+
13
+ before(:each) do
14
+ @machine = mock("machine")
15
+ @state = new_state(:off, @machine)
16
+ end
17
+
18
+ it "#initialize should work" do
19
+ @state.name.should == :off
20
+ @state.machine.should == @machine
21
+ end
22
+ end
@@ -0,0 +1,56 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent.parent + 'spec_helper'
3
+
4
+ describe "EventDsl" do
5
+
6
+ describe "event" do
7
+
8
+ before(:each) do
9
+ class Earth
10
+ extend DataMapper::Is::StateMachine::EventDsl
11
+ stub!(:state_machine_context?).and_return(true)
12
+ stub!(:push_state_machine_context)
13
+ stub!(:pop_state_machine_context)
14
+ end
15
+ machine = mock("machine", :events => [], :column => :state)
16
+ Earth.instance_variable_set(:@is_state_machine, { :machine => machine })
17
+ end
18
+
19
+ it "declaration should succeed" do
20
+ class Earth
21
+ event :sunrise
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ describe "transition" do
28
+
29
+ before(:each) do
30
+
31
+ class Earth
32
+ extend DataMapper::Is::StateMachine::EventDsl
33
+
34
+ stub!(:state_machine_context?).and_return(true)
35
+ stub!(:push_state_machine_context)
36
+ stub!(:pop_state_machine_context)
37
+ end
38
+
39
+ machine = mock("machine", :events => [], :column => :state)
40
+ event = mock("sunrise_event")
41
+ event.stub!(:add_transition)
42
+ Earth.instance_variable_set(:@is_state_machine, {
43
+ :machine => machine,
44
+ :event => { :name => :sunrise, :object => event }
45
+ })
46
+ end
47
+
48
+ it "transition definition should succeed" do
49
+ class Earth
50
+ transition :from => :night, :to => :day
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,25 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent.parent + 'spec_helper'
3
+
4
+ describe "StateDsl" do
5
+
6
+ describe "state" do
7
+
8
+ before(:each) do
9
+ class Earth
10
+ extend DataMapper::Is::StateMachine::StateDsl
11
+ stub!(:state_machine_context?).and_return(true)
12
+ end
13
+ machine = mock("machine", :states => [])
14
+ Earth.instance_variable_set(:@is_state_machine, { :machine => machine })
15
+ end
16
+
17
+ it "declaration should succeed" do
18
+ class Earth
19
+ state :day
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,34 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
3
+
4
+ describe "StateMachine" do
5
+
6
+ describe "is_state_machine" do
7
+
8
+ before(:each) do
9
+ class Earth
10
+ extend DataMapper::Is::StateMachine
11
+
12
+ stub!(:properties).and_return([])
13
+ stub!(:property)
14
+ stub!(:before)
15
+
16
+ stub!(:state_machine_context?).and_return(true)
17
+ stub!(:push_state_machine_context)
18
+ stub!(:pop_state_machine_context)
19
+ end
20
+ end
21
+
22
+ it "declaration should succeed" do
23
+ class Earth
24
+ is_state_machine
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+
31
+ # is_state_machine
32
+ # push_state_machine_context(label)
33
+ # pop_state_machine_context
34
+ # state_machine_context?(label)
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm-is-state_machine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.4
5
+ platform: ruby
6
+ authors:
7
+ - David James
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-08-21 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.4
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.7.0
34
+ version:
35
+ description: DataMapper plugin for creating state machines
36
+ email:
37
+ - djwonk [a] collectiveinsight [d] net
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - README.txt
44
+ - LICENSE
45
+ - TODO
46
+ files:
47
+ - History.txt
48
+ - LICENSE
49
+ - Manifest.txt
50
+ - README.txt
51
+ - Rakefile
52
+ - TODO
53
+ - lib/dm-is-state_machine.rb
54
+ - lib/dm-is-state_machine/is/data/event.rb
55
+ - lib/dm-is-state_machine/is/data/machine.rb
56
+ - lib/dm-is-state_machine/is/data/state.rb
57
+ - lib/dm-is-state_machine/is/dsl/event_dsl.rb
58
+ - lib/dm-is-state_machine/is/dsl/state_dsl.rb
59
+ - lib/dm-is-state_machine/is/state_machine.rb
60
+ - lib/dm-is-state_machine/is/version.rb
61
+ - spec/examples/invalid_events.rb
62
+ - spec/examples/invalid_states.rb
63
+ - spec/examples/invalid_transitions_1.rb
64
+ - spec/examples/invalid_transitions_2.rb
65
+ - spec/examples/traffic_light.rb
66
+ - spec/integration/invalid_events_spec.rb
67
+ - spec/integration/invalid_states_spec.rb
68
+ - spec/integration/invalid_transitions_spec.rb
69
+ - spec/integration/traffic_light_spec.rb
70
+ - spec/spec.opts
71
+ - spec/spec_helper.rb
72
+ - spec/unit/data/event_spec.rb
73
+ - spec/unit/data/machine_spec.rb
74
+ - spec/unit/data/state_spec.rb
75
+ - spec/unit/dsl/event_dsl_spec.rb
76
+ - spec/unit/dsl/state_dsl_spec.rb
77
+ - spec/unit/state_machine_spec.rb
78
+ has_rdoc: true
79
+ homepage: http://github.com/sam/dm-more/tree/master/dm-is-state_machine
80
+ post_install_message:
81
+ rdoc_options:
82
+ - --main
83
+ - README.txt
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ version:
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ requirements: []
99
+
100
+ rubyforge_project: datamapper
101
+ rubygems_version: 1.2.0
102
+ signing_key:
103
+ specification_version: 2
104
+ summary: DataMapper plugin for creating state machines
105
+ test_files: []
106
+