state_machine 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/CHANGELOG.rdoc +31 -1
  2. data/README.rdoc +33 -21
  3. data/Rakefile +2 -2
  4. data/examples/merb-rest/controller.rb +51 -0
  5. data/examples/merb-rest/model.rb +28 -0
  6. data/examples/merb-rest/view_edit.html.erb +24 -0
  7. data/examples/merb-rest/view_index.html.erb +23 -0
  8. data/examples/merb-rest/view_new.html.erb +13 -0
  9. data/examples/merb-rest/view_show.html.erb +17 -0
  10. data/examples/rails-rest/controller.rb +43 -0
  11. data/examples/rails-rest/migration.rb +11 -0
  12. data/examples/rails-rest/model.rb +23 -0
  13. data/examples/rails-rest/view_edit.html.erb +25 -0
  14. data/examples/rails-rest/view_index.html.erb +23 -0
  15. data/examples/rails-rest/view_new.html.erb +14 -0
  16. data/examples/rails-rest/view_show.html.erb +17 -0
  17. data/lib/state_machine/assertions.rb +2 -2
  18. data/lib/state_machine/callback.rb +14 -8
  19. data/lib/state_machine/condition_proxy.rb +3 -3
  20. data/lib/state_machine/event.rb +19 -21
  21. data/lib/state_machine/event_collection.rb +114 -0
  22. data/lib/state_machine/extensions.rb +127 -11
  23. data/lib/state_machine/guard.rb +1 -1
  24. data/lib/state_machine/integrations/active_record/locale.rb +2 -1
  25. data/lib/state_machine/integrations/active_record.rb +117 -39
  26. data/lib/state_machine/integrations/data_mapper/observer.rb +20 -64
  27. data/lib/state_machine/integrations/data_mapper.rb +71 -26
  28. data/lib/state_machine/integrations/sequel.rb +69 -21
  29. data/lib/state_machine/machine.rb +267 -139
  30. data/lib/state_machine/machine_collection.rb +145 -0
  31. data/lib/state_machine/matcher.rb +2 -2
  32. data/lib/state_machine/node_collection.rb +9 -4
  33. data/lib/state_machine/state.rb +22 -32
  34. data/lib/state_machine/state_collection.rb +66 -17
  35. data/lib/state_machine/transition.rb +259 -28
  36. data/lib/state_machine.rb +121 -56
  37. data/tasks/state_machine.rake +1 -0
  38. data/tasks/state_machine.rb +26 -0
  39. data/test/active_record.log +116877 -0
  40. data/test/functional/state_machine_test.rb +118 -12
  41. data/test/sequel.log +28542 -0
  42. data/test/unit/callback_test.rb +46 -1
  43. data/test/unit/condition_proxy_test.rb +55 -28
  44. data/test/unit/event_collection_test.rb +228 -0
  45. data/test/unit/event_test.rb +51 -46
  46. data/test/unit/integrations/active_record_test.rb +128 -70
  47. data/test/unit/integrations/data_mapper_test.rb +150 -58
  48. data/test/unit/integrations/sequel_test.rb +63 -6
  49. data/test/unit/invalid_event_test.rb +7 -0
  50. data/test/unit/machine_collection_test.rb +678 -0
  51. data/test/unit/machine_test.rb +198 -91
  52. data/test/unit/node_collection_test.rb +33 -30
  53. data/test/unit/state_collection_test.rb +112 -5
  54. data/test/unit/state_test.rb +23 -3
  55. data/test/unit/transition_test.rb +750 -89
  56. metadata +28 -3
data/CHANGELOG.rdoc CHANGED
@@ -1,6 +1,36 @@
1
1
  == master
2
2
 
3
- == 0.6.3 / 2008-03-10
3
+ == 0.7.0 / 2009-04-03
4
+
5
+ * Add #{attribute}_event for automatically firing events when the object's action is called
6
+ * Make it easier to override state-driven behaviors
7
+ * Rollback state changes when the action fails during transitions
8
+ * Use :messages instead of :invalid_message for customizing validation errors
9
+ * Use more human-readable validation errors
10
+ * Add support for more ActiveRecord observer hooks
11
+ * Add support for targeting multiple specific state machines in DataMapper observer hooks
12
+ * Don't pass the result of the action as an argument to callbacks (access via Transition#result)
13
+ * Fix incorrect results being used when running transitions in parallel
14
+ * Fix transition args not being set when run in parallel
15
+ * Allow callback terminators to be set on an application-wide basis
16
+ * Only catch :halt during before / after transition callbacks
17
+ * Fix ActiveRecord predicates being overwritten if they're already defined in the class
18
+ * Allow machine options to be set on an integration-wide basis
19
+ * Turn transactions off by default in DataMapper integrations
20
+ * Add support for configuring the use of transactions
21
+ * Simplify reading/writing of attributes
22
+ * Simplify access to state machines via #state_machine(:attribute) without generating dupes
23
+ * Fix assumptions that dm-validations is always available in DataMapper integration
24
+ * Automatically define DataMapper properties for machine attributes if they don't exist
25
+ * Add Transition#qualified_event, #qualified_from_name, and #qualified_to_name
26
+ * Add #fire_events / #fire_events! for running events on multiple state machines in parallel
27
+ * Rename next_#{event}_transition to #{event}_transition
28
+ * Add #{attribute}_transitions for getting the list of transitions that can be run on an object
29
+ * Add #{attribute}_events for getting the list of events that can be fired on an object
30
+ * Use generated non-bang event when running bang version so that overriding one affects the other
31
+ * Provide access to arguments passed into an event from transition callbacks via Transition#args
32
+
33
+ == 0.6.3 / 2009-03-10
4
34
 
5
35
  * Add support for customizing the graph's orientation
6
36
  * Use the standard visualizations for initial (open arrow) and final (double circle) states
data/README.rdoc CHANGED
@@ -45,6 +45,8 @@ Some brief, high-level features include:
45
45
  * State-driven instance / class behavior
46
46
  * State values of any data type
47
47
  * Dynamically-generated state values
48
+ * Event parallelization
49
+ * Attribute-based event transitions
48
50
  * Inheritance
49
51
  * Internationalization
50
52
  * GraphViz visualization creator
@@ -63,6 +65,7 @@ Below is an example of many of the features offered by this plugin, including:
63
65
  * Conditional transitions
64
66
  * State-driven instance behavior
65
67
  * Customized state values
68
+ * Parallel events
66
69
 
67
70
  Class definition:
68
71
 
@@ -125,17 +128,17 @@ Class definition:
125
128
  end
126
129
  end
127
130
 
128
- state_machine :hood_state, :initial => :closed, :namespace => 'hood' do
129
- event :open do
130
- transition all => :opened
131
+ state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
132
+ event :enable do
133
+ transition all => :active
131
134
  end
132
135
 
133
- event :close do
134
- transition all => :closed
136
+ event :disable do
137
+ transition all => :off
135
138
  end
136
139
 
137
- state :opened, :value => 1
138
- state :closed, :value => 0
140
+ state :active, :value => 1
141
+ state :off, :value => 0
139
142
  end
140
143
 
141
144
  def initialize
@@ -168,7 +171,9 @@ like so:
168
171
  vehicle.state_name # => :parked
169
172
  vehicle.parked? # => true
170
173
  vehicle.can_ignite? # => true
171
- vehicle.next_ignite_transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
174
+ vehicle.ignite_transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
175
+ vehicle.state_events # => [:ignite]
176
+ vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
172
177
  vehicle.speed # => 0
173
178
 
174
179
  vehicle.ignite # => true
@@ -189,21 +194,28 @@ like so:
189
194
  vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from :second_gear
190
195
 
191
196
  # Generic state predicates can raise exceptions if the value does not exist
192
- vehicle.state?(:parked) # => true
193
- vehicle.state?(:invalid) # => ArgumentError: :invalid is an invalid name
197
+ vehicle.state?(:parked) # => false
198
+ vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
194
199
 
195
200
  # Namespaced machines have uniquely-generated methods
196
- vehicle.hood_state # => 0
197
- vehicle.hood_state_name # => :closed
201
+ vehicle.alarm_state # => 1
202
+ vehicle.alarm_state_name # => :active
198
203
 
199
- vehicle.can_open_hood? # => true
200
- vehicle.open_hood # => true
201
- vehicle.hood_state # => 1
202
- vehicle.hood_state_name # => :opened
203
- vehicle.can_close_hood? # => true
204
+ vehicle.can_disable_alarm? # => true
205
+ vehicle.disable_alarm # => true
206
+ vehicle.alarm_state # => 0
207
+ vehicle.alarm_state_name # => :off
208
+ vehicle.can_enable_alarm? # => true
204
209
 
205
- vehicle.hood_opened? # => true
206
- vehicle.hood_closed? # => false
210
+ vehicle.alarm_off? # => true
211
+ vehicle.alarm_active? # => false
212
+
213
+ # Events can be fired in parallel
214
+ vehicle.fire_events(:shift_down, :enable_alarm) # => true
215
+ vehicle.state_name # => :first_gear
216
+ vehicle.alarm_state_name # => :active
217
+
218
+ vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachine::InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
207
219
 
208
220
  *Note* the comment made on the +initialize+ method in the class. In order for
209
221
  state machine attributes to be properly initialized, <tt>super()</tt> must be called.
@@ -307,8 +319,8 @@ callbacks, validation errors, and observers. For example,
307
319
  end
308
320
 
309
321
  # Generic transition callback *before* the transition is performed
310
- after_transition do |transition, saved|
311
- Audit.log(self, transition) if saved # self is the record
322
+ after_transition do |transition|
323
+ Audit.log(self, transition) # self is the record
312
324
  end
313
325
  end
314
326
 
data/Rakefile CHANGED
@@ -5,11 +5,11 @@ require 'rake/contrib/sshpublisher'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = 'state_machine'
8
- s.version = '0.6.3'
8
+ s.version = '0.7.0'
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
11
11
 
12
- s.files = FileList['{examples,lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
12
+ s.files = FileList['{examples,lib,tasks,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
13
13
  s.require_path = 'lib'
14
14
  s.has_rdoc = true
15
15
  s.test_files = Dir['test/**/*_test.rb']
@@ -0,0 +1,51 @@
1
+ class Users < Application
2
+ # GET /users
3
+ def index
4
+ @users = User.all
5
+ display @users
6
+ end
7
+
8
+ # GET /users/1
9
+ def show(id)
10
+ @user = User.get(id)
11
+ raise NotFound unless @user
12
+ display @user
13
+ end
14
+
15
+ # GET /users/new
16
+ def new
17
+ only_provides :html
18
+ @user = User.new
19
+ display @user
20
+ end
21
+
22
+ # GET /users/1/edit
23
+ def edit(id)
24
+ only_provides :html
25
+ @user = User.get(id)
26
+ raise NotFound unless @user
27
+ display @user
28
+ end
29
+
30
+ # POST /users
31
+ def create(user)
32
+ @user = User.new(user)
33
+ if @user.save
34
+ redirect resource(@user), :message => {:notice => "User was successfully created"}
35
+ else
36
+ message[:error] = "User failed to be created"
37
+ render :new
38
+ end
39
+ end
40
+
41
+ # PUT /users/1
42
+ def update(id, user)
43
+ @user = User.get(id)
44
+ raise NotFound unless @user
45
+ if @user.update_attributes(user)
46
+ redirect resource(@user)
47
+ else
48
+ display @user, :edit
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,28 @@
1
+ class User
2
+ include DataMapper::Resource
3
+
4
+ property :id, Serial
5
+ property :name, String
6
+
7
+ validates_present :name, :state, :access_state
8
+
9
+ state_machine :initial => :unregistered do
10
+ event :register do
11
+ transition :unregistered => :registered
12
+ end
13
+
14
+ event :unregister do
15
+ transition :registered => :unregistered
16
+ end
17
+ end
18
+
19
+ state_machine :access_state, :initial => :enabled do
20
+ event :enable do
21
+ transition all => :enabled
22
+ end
23
+
24
+ event :disable do
25
+ transition all => :disabled
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ <h1>Editing User</h1>
2
+
3
+ <%= form_for @user, :action => resource(@user) do %>
4
+ <%= error_messages %>
5
+
6
+ <p>
7
+ <%= text_field :name, :label => 'Name' %>
8
+ </p>
9
+
10
+ <p>
11
+ <%= label :state %><br />
12
+ <%= select :state_event, :selected => @user.state_event.to_s, :collection => @user.state_transitions, :value_method => :event, :text_method => :to_name, :prompt => @user.state_name.to_s %>
13
+ </p>
14
+
15
+ <p>
16
+ <%= label :access_state %><br />
17
+ <%= select :access_state_event, :selected => @user.access_state_event.to_s, :collection => @user.access_state_transitions, :value_method => :event, :text_method => :event, :prompt => "don't change" %>
18
+ </p>
19
+
20
+ <p><%= submit 'Update' %></p>
21
+ <% end =%>
22
+
23
+ <%= link_to 'Show', resource(@user) %> |
24
+ <%= link_to 'Back', resource(:users) %>
@@ -0,0 +1,23 @@
1
+ <h1>Listing Users</h1>
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Name</th>
6
+ <th>State</th>
7
+ <th>Access State</th>
8
+ </tr>
9
+
10
+ <% @users.each do |user| %>
11
+ <tr>
12
+ <td><%=h user.name %></td>
13
+ <td><%=h user.state %></td>
14
+ <td><%=h user.access_state %></td>
15
+ <td><%= link_to 'Show', resource(user) %></td>
16
+ <td><%= link_to 'Edit', resource(user, :edit) %></td>
17
+ </tr>
18
+ <% end %>
19
+ </table>
20
+
21
+ <br />
22
+
23
+ <%= link_to 'New User', resource(:users, :new) %>
@@ -0,0 +1,13 @@
1
+ <h1>New User</h1>
2
+
3
+ <%= form_for @user, :action => resource(:users) do %>
4
+ <%= error_messages %>
5
+
6
+ <p>
7
+ <%= text_field :name, :label => 'Name' %>
8
+ </p>
9
+
10
+ <p><%= submit 'Create' %></p>
11
+ <% end =%>
12
+
13
+ <%= link_to 'Back', resource(:users) %>
@@ -0,0 +1,17 @@
1
+ <p>
2
+ <b>Name:</b>
3
+ <%=h @user.name %>
4
+ </p>
5
+
6
+ <p>
7
+ <b>State:</b>
8
+ <%=h @user.state %>
9
+ </p>
10
+
11
+ <p>
12
+ <b>Access State:</b>
13
+ <%=h @user.access_state %>
14
+ </p>
15
+
16
+ <%= link_to 'Edit', resource(@user, :edit) %> |
17
+ <%= link_to 'Back', resource(:users) %>
@@ -0,0 +1,43 @@
1
+ class UsersController < ApplicationController
2
+ # GET /users
3
+ def index
4
+ @users = User.all
5
+ end
6
+
7
+ # GET /users/1
8
+ def show
9
+ @user = User.find(params[:id])
10
+ end
11
+
12
+ # GET /users/new
13
+ def new
14
+ @user = User.new
15
+ end
16
+
17
+ # GET /users/1/edit
18
+ def edit
19
+ @user = User.find(params[:id])
20
+ end
21
+
22
+ # POST /users
23
+ def create
24
+ @user = User.new(params[:user])
25
+
26
+ if @user.save
27
+ redirect_to(@user)
28
+ else
29
+ render :action => 'new'
30
+ end
31
+ end
32
+
33
+ # PUT /users/1
34
+ def update
35
+ @user = User.find(params[:id])
36
+
37
+ if @user.update_attributes(params[:user])
38
+ redirect_to(@user)
39
+ else
40
+ render :action => 'edit'
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.string :name, :state, :access_state
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :users
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ class User < ActiveRecord::Base
2
+ validates_presence_of :name, :state, :access_state
3
+
4
+ state_machine :initial => :unregistered do
5
+ event :register do
6
+ transition :unregistered => :registered
7
+ end
8
+
9
+ event :unregister do
10
+ transition :registered => :unregistered
11
+ end
12
+ end
13
+
14
+ state_machine :access_state, :initial => :enabled do
15
+ event :enable do
16
+ transition all => :enabled
17
+ end
18
+
19
+ event :disable do
20
+ transition all => :disabled
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ <h1>Editing User</h1>
2
+
3
+ <% form_for(@user) do |f| %>
4
+ <%= f.error_messages %>
5
+
6
+ <p>
7
+ <%= f.label :name %><br />
8
+ <%= f.text_field :name %>
9
+ </p>
10
+
11
+ <p>
12
+ <%= f.label :state %><br />
13
+ <%= f.collection_select :state_event, @user.state_transitions, :event, :to_name, :include_blank => @user.state_name.to_s %>
14
+ </p>
15
+
16
+ <p>
17
+ <%= f.label :access_state %><br />
18
+ <%= f.collection_select :access_state_event, @user.access_state_transitions, :event, :event, :include_blank => "don't change" %>
19
+ </p>
20
+
21
+ <p><%= f.submit 'Update' %></p>
22
+ <% end %>
23
+
24
+ <%= link_to 'Show', @user %> |
25
+ <%= link_to 'Back', users_path %>
@@ -0,0 +1,23 @@
1
+ <h1>Listing Users</h1>
2
+
3
+ <table>
4
+ <tr>
5
+ <th>Name</th>
6
+ <th>State</th>
7
+ <th>Access State</th>
8
+ </tr>
9
+
10
+ <% @users.each do |user| %>
11
+ <tr>
12
+ <td><%=h user.name %></td>
13
+ <td><%=h user.state %></td>
14
+ <td><%=h user.access_state %></td>
15
+ <td><%= link_to 'Show', user %></td>
16
+ <td><%= link_to 'Edit', edit_user_path(user) %></td>
17
+ </tr>
18
+ <% end %>
19
+ </table>
20
+
21
+ <br />
22
+
23
+ <%= link_to 'New User', new_user_path %>