halorgium-dm-is-state_machine 0.10.2.via

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 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/README.rdoc 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
@@ -0,0 +1,8 @@
1
+ require 'dm-is-state_machine/is/state_machine'
2
+ require 'dm-is-state_machine/is/data/event'
3
+ require 'dm-is-state_machine/is/data/machine'
4
+ require 'dm-is-state_machine/is/data/state'
5
+ require 'dm-is-state_machine/is/dsl/event_dsl'
6
+ require 'dm-is-state_machine/is/dsl/state_dsl'
7
+
8
+ 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, via)
17
+ @transitions << { :from => from, :to => to, :via => via }
18
+ end
19
+
20
+ end
21
+
22
+ end # Data
23
+ end # StateMachine
24
+ end # Is
25
+ end # DataMapper
@@ -0,0 +1,142 @@
1
+ module DataMapper
2
+ module Is
3
+ module StateMachine
4
+ module Data
5
+
6
+ class Machine
7
+ def initialize(definition, resource)
8
+ @definition = definition
9
+ @resource = resource
10
+ end
11
+
12
+ def run_initial
13
+ return unless initial
14
+ return unless initial_state = @definition.find_state(initial)
15
+ run_hook_if_present initial_state.options[:enter]
16
+ end
17
+
18
+ # hook may be either a Proc or symbol
19
+ def run_hook_if_present(hook)
20
+ return unless hook
21
+ if hook.respond_to?(:call)
22
+ hook.call(@resource)
23
+ else
24
+ @resource.__send__(hook)
25
+ end
26
+ end
27
+
28
+ def initial
29
+ @definition.initial
30
+ end
31
+
32
+ def find_event(event_name)
33
+ @definition.find_event(event_name)
34
+ end
35
+
36
+ def state?(state_name)
37
+ if @definition.find_state(state_name)
38
+ current_state_name == state_name.to_s
39
+ else
40
+ raise InvalidState.new("Invalid state: #{state_name.inspect}")
41
+ end
42
+ end
43
+
44
+ def fire_event(event_name)
45
+ transition = @definition.fire_event(event_name, current_state_name)
46
+
47
+ if via_state_name = transition[:via]
48
+ self.current_state_name = via_state_name
49
+ end
50
+
51
+ # == Change the current_state ==
52
+ self.current_state_name = transition[:to]
53
+ end
54
+
55
+ # Return the current state
56
+ #
57
+ # @api public
58
+ def current_state
59
+ @definition.find_state(current_state_name)
60
+ # TODO: add caching, i.e. with `@current_state ||= ...`
61
+ end
62
+
63
+ def current_state_name
64
+ @resource.attribute_get(@definition.column).to_s
65
+ end
66
+
67
+ def current_state_name=(state_name)
68
+ # == Run :exit hook (if present) ==
69
+ run_hook_if_present current_state.options[:exit]
70
+
71
+ @resource.update(@definition.column => state_name.to_s)
72
+
73
+ # == Run :enter hook (if present) ==
74
+ run_hook_if_present current_state.options[:enter]
75
+ end
76
+ end
77
+
78
+ # This Machine class represents one state machine.
79
+ #
80
+ # A model (i.e. a DataMapper resource) can have more than one Machine.
81
+ class MachineDefinition
82
+
83
+ # The property of the DM resource that will hold this Machine's
84
+ # state.
85
+ #
86
+ # TODO: change :column to :property
87
+ attr_accessor :column
88
+
89
+ # The initial value of this Machine's state
90
+ attr_accessor :initial
91
+
92
+ attr_accessor :events
93
+
94
+ attr_accessor :states
95
+
96
+ def initialize(column, initial)
97
+ @column, @initial = column, initial
98
+ @events, @states = [], []
99
+ end
100
+
101
+ # Fire (activate) the event with name +event_name+
102
+ #
103
+ # @api public
104
+ def fire_event(event_name, current_state_name)
105
+ event_name = event_name.to_s
106
+ unless event = find_event(event_name)
107
+ raise InvalidEvent, "Could not find event (#{event_name.inspect})"
108
+ end
109
+ transition = event.transitions.find do |t|
110
+ Array(t[:from]).any? do |from_state|
111
+ from_state.to_s == current_state_name
112
+ end
113
+ end
114
+ unless transition
115
+ raise InvalidEvent, "Event (#{event_name.inspect}) does not " +
116
+ "exist for current state (#{current_state_name.inspect})"
117
+ end
118
+ transition
119
+ end
120
+
121
+ # Find event whose name is +event_name+
122
+ #
123
+ # @api semipublic
124
+ def find_event(event_name)
125
+ @events.find { |event| event.name.to_s == event_name.to_s }
126
+ # TODO: use a data structure that prevents duplicates
127
+ end
128
+
129
+ # Find state whose name is +event_name+
130
+ #
131
+ # @api semipublic
132
+ def find_state(state_name)
133
+ @states.find { |state| state.name.to_s == state_name.to_s }
134
+ # TODO: use a data structure that prevents duplicates
135
+ end
136
+
137
+ end
138
+
139
+ end # Data
140
+ end # StateMachine
141
+ end # Is
142
+ 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,110 @@
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
+ if method_defined?("#{name}!")
35
+ raise InvalidEvent, "There is a method called #{name}! on #{self}"
36
+ end
37
+
38
+ event_object = create_event(name)
39
+
40
+ # ===== Setup context =====
41
+ @is_state_machine[:event] = {
42
+ :name => name,
43
+ :object => event_object
44
+ }
45
+ push_state_machine_context(:event)
46
+
47
+ # ===== Define methods =====
48
+ define_method("#{name}!") do
49
+ transition!(name)
50
+ end
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 = __send__(:"#{column}")
58
+ # machine.fire_event(name, self)
59
+ # __send__(:"#{column}="), machine.current_state_name
60
+ # end
61
+ # RUBY
62
+
63
+ yield if block_given?
64
+
65
+ # ===== Teardown context =====
66
+ pop_state_machine_context
67
+ end
68
+
69
+ def destroy(options)
70
+ unless state_machine_context?(:is)
71
+ raise InvalidContext, "Valid only in 'is :state_machine' block"
72
+ end
73
+
74
+ event_object = create_event(:destroy)
75
+ from = options[:from]
76
+ to = options[:to]
77
+ via = options[:via]
78
+ event_object.add_transition(from, to, via)
79
+ end
80
+
81
+ def create_event(name)
82
+ unless state_machine_context?(:is)
83
+ raise InvalidContext, "Valid only in 'is :state_machine' block"
84
+ end
85
+
86
+ name = name.to_s
87
+
88
+ definition = @is_state_machine[:definition]
89
+ event = Data::Event.new(name, definition)
90
+ definition.events << event
91
+ event
92
+ end
93
+
94
+ def transition(options)
95
+ unless state_machine_context?(:event)
96
+ raise InvalidContext, "Valid only in 'event' block"
97
+ end
98
+ event_name = @is_state_machine[:event][:name]
99
+ event_object = @is_state_machine[:event][:object]
100
+
101
+ from = options[:from]
102
+ to = options[:to]
103
+ via = options[:via]
104
+ event_object.add_transition(from, to, via)
105
+ end
106
+
107
+ end # EventDsl
108
+ end # StateMachine
109
+ end # Is
110
+ 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
+ definition = @is_state_machine[:definition]
33
+ state = Data::State.new(name, definition, options)
34
+ definition.states << state
35
+ end
36
+
37
+ end # StateDsl
38
+ end # StateMachine
39
+ end # Is
40
+ end # DataMapper
@@ -0,0 +1,126 @@
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
+ definition = Data::MachineDefinition.new(column, initial)
39
+ @is_state_machine = { :definition => definition }
40
+
41
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
42
+ def #{column}=(value)
43
+ value = value.to_s if value.kind_of?(Symbol)
44
+ attribute_set(#{column.inspect}, value)
45
+ end
46
+ RUBY
47
+
48
+ # ===== Define callbacks =====
49
+ # TODO: define callbacks
50
+ # before :save do
51
+ # if self.new_record?
52
+ # # ...
53
+ # else
54
+ # # ...
55
+ # end
56
+ # end
57
+
58
+ before :destroy do
59
+ if state_machine.find_event(:destroy)
60
+ transition!(:destroy)
61
+ end
62
+ end
63
+
64
+ # ===== Setup context =====
65
+ push_state_machine_context(:is)
66
+
67
+ yield if block_given?
68
+
69
+ # ===== Teardown context =====
70
+ pop_state_machine_context
71
+ end
72
+
73
+ protected
74
+
75
+ def push_state_machine_context(label)
76
+ @is_state_machine ||= {}
77
+ @is_state_machine[:context] ||= []
78
+ @is_state_machine[:context] << label
79
+
80
+ # Compacted, but barely readable for humans
81
+ # ((@is_state_machine ||= {})[:context] ||= []) << label
82
+ end
83
+
84
+ def pop_state_machine_context
85
+ @is_state_machine[:context].pop
86
+ end
87
+
88
+ def state_machine_context?(label)
89
+ (i = @is_state_machine) && (c = i[:context]) &&
90
+ c.respond_to?(:include?) && c.include?(label)
91
+ end
92
+
93
+ module InstanceMethods
94
+
95
+ def initialize(*args)
96
+ super
97
+ # ===== Run :enter hook if present =====
98
+ state_machine.run_initial
99
+ end
100
+
101
+ def transition!(event_name)
102
+ state_machine.fire_event(event_name)
103
+ end
104
+
105
+ def state?(state_name)
106
+ state_machine.state?(state_name)
107
+ end
108
+
109
+ def state_machine
110
+ return unless is_sm = model.instance_variable_get(:@is_state_machine)
111
+ return unless definition = is_sm[:definition]
112
+ Data::Machine.new(definition, self)
113
+ end
114
+
115
+ end # InstanceMethods
116
+
117
+ end # StateMachine
118
+ end # Is
119
+ end # DataMapper
120
+
121
+ # Notes
122
+ # -----
123
+ #
124
+ # Since this gets mixed into a class, I try to keep the namespace pollution
125
+ # down to a minimum. This is why I only use the @is_state_machine instance
126
+ # variable.
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: halorgium-dm-is-state_machine
3
+ version: !ruby/object:Gem::Version
4
+ hash: 1012425149
5
+ prerelease: 7
6
+ segments:
7
+ - 0
8
+ - 10
9
+ - 2
10
+ - via
11
+ version: 0.10.2.via
12
+ platform: ruby
13
+ authors:
14
+ - David James
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2009-12-11 00:00:00 -08:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: dm-core
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 51
31
+ segments:
32
+ - 0
33
+ - 10
34
+ - 2
35
+ version: 0.10.2
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: rspec
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ hash: 13
47
+ segments:
48
+ - 1
49
+ - 2
50
+ - 9
51
+ version: 1.2.9
52
+ type: :development
53
+ version_requirements: *id002
54
+ - !ruby/object:Gem::Dependency
55
+ name: yard
56
+ prerelease: false
57
+ requirement: &id003 !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ hash: 15
63
+ segments:
64
+ - 0
65
+ - 4
66
+ - 0
67
+ version: 0.4.0
68
+ type: :development
69
+ version_requirements: *id003
70
+ description: DataMapper plugin for creating state machines
71
+ email: djwonk [a] collectiveinsight [d] net
72
+ executables: []
73
+
74
+ extensions: []
75
+
76
+ extra_rdoc_files:
77
+ - LICENSE
78
+ - README.rdoc
79
+ files:
80
+ - lib/dm-is-state_machine.rb
81
+ - lib/dm-is-state_machine/is/data/event.rb
82
+ - lib/dm-is-state_machine/is/data/machine.rb
83
+ - lib/dm-is-state_machine/is/data/state.rb
84
+ - lib/dm-is-state_machine/is/dsl/event_dsl.rb
85
+ - lib/dm-is-state_machine/is/dsl/state_dsl.rb
86
+ - lib/dm-is-state_machine/is/state_machine.rb
87
+ - LICENSE
88
+ - README.rdoc
89
+ has_rdoc: true
90
+ homepage: http://github.com/halorgium/dm-is-state_machine/tree/via
91
+ licenses: []
92
+
93
+ post_install_message:
94
+ rdoc_options:
95
+ - --charset=UTF-8
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ hash: 3
113
+ segments:
114
+ - 0
115
+ version: "0"
116
+ requirements: []
117
+
118
+ rubyforge_project: datamapper
119
+ rubygems_version: 1.5.0
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: DataMapper plugin for creating state machines
123
+ test_files: []
124
+