dm-is-state_machine 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
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
+