verborghs-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/CHANGELOG.rdoc +360 -0
- data/LICENSE +20 -0
- data/README.rdoc +635 -0
- data/Rakefile +77 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +241 -0
- data/lib/state_machine/condition_proxy.rb +106 -0
- data/lib/state_machine/eval_helpers.rb +83 -0
- data/lib/state_machine/event.rb +267 -0
- data/lib/state_machine/event_collection.rb +122 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/guard.rb +230 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +5 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +45 -0
- data/lib/state_machine/integrations/active_model.rb +445 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record.rb +522 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
- data/lib/state_machine/integrations/data_mapper.rb +379 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
- data/lib/state_machine/integrations/sequel.rb +356 -0
- data/lib/state_machine/integrations.rb +83 -0
- data/lib/state_machine/machine.rb +1645 -0
- data/lib/state_machine/machine_collection.rb +64 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +260 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +399 -0
- data/lib/state_machine/transition_collection.rb +244 -0
- data/lib/state_machine.rb +421 -0
- data/lib/tasks/state_machine.rake +1 -0
- data/lib/tasks/state_machine.rb +27 -0
- data/test/files/en.yml +9 -0
- data/test/files/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +980 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +728 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +222 -0
- data/test/unit/event_collection_test.rb +324 -0
- data/test/unit/event_test.rb +795 -0
- data/test/unit/guard_test.rb +909 -0
- data/test/unit/integrations/active_model_test.rb +956 -0
- data/test/unit/integrations/active_record_test.rb +1918 -0
- data/test/unit/integrations/data_mapper_test.rb +1814 -0
- data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
- data/test/unit/integrations/sequel_test.rb +1492 -0
- data/test/unit/integrations_test.rb +50 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +565 -0
- data/test/unit/machine_test.rb +2349 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +848 -0
- data/test/unit/transition_collection_test.rb +2098 -0
- data/test/unit/transition_test.rb +1384 -0
- metadata +176 -0
Binary file
|
data/examples/car.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class Car < Vehicle
|
2
|
+
state_machine do
|
3
|
+
event :reverse do
|
4
|
+
transition [:parked, :idling, :first_gear] => :backing_up
|
5
|
+
end
|
6
|
+
|
7
|
+
event :park do
|
8
|
+
transition :backing_up => :parked
|
9
|
+
end
|
10
|
+
|
11
|
+
event :idle do
|
12
|
+
transition :backing_up => :idling
|
13
|
+
end
|
14
|
+
|
15
|
+
event :shift_up do
|
16
|
+
transition :backing_up => :first_gear
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -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 => :human_to_name, :prompt => @user.human_state_name %>
|
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 => :human_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.human_state_name %></td>
|
14
|
+
<td><%=h user.human_access_state_name %></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,17 @@
|
|
1
|
+
<p>
|
2
|
+
<b>Name:</b>
|
3
|
+
<%=h @user.name %>
|
4
|
+
</p>
|
5
|
+
|
6
|
+
<p>
|
7
|
+
<b>State:</b>
|
8
|
+
<%=h @user.human_state_name %>
|
9
|
+
</p>
|
10
|
+
|
11
|
+
<p>
|
12
|
+
<b>Access State:</b>
|
13
|
+
<%=h @user.human_access_state_name %>
|
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,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, :human_to_name, :include_blank => @user.human_state_name %>
|
14
|
+
</p>
|
15
|
+
|
16
|
+
<p>
|
17
|
+
<%= f.label :access_state %><br />
|
18
|
+
<%= f.collection_select :access_state_event, @user.access_state_transitions, :event, :human_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.human_state_name %></td>
|
14
|
+
<td><%=h user.human_access_state_name %></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 %>
|
@@ -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.human_state_name %>
|
9
|
+
</p>
|
10
|
+
|
11
|
+
<p>
|
12
|
+
<b>Access State:</b>
|
13
|
+
<%=h @user.human_access_state_name %>
|
14
|
+
</p>
|
15
|
+
|
16
|
+
<%= link_to 'Edit', edit_user_path(@user) %> |
|
17
|
+
<%= link_to 'Back', users_path %>
|
data/examples/vehicle.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
class Vehicle
|
2
|
+
state_machine :initial => :parked do
|
3
|
+
event :park do
|
4
|
+
transition [:idling, :first_gear] => :parked
|
5
|
+
end
|
6
|
+
|
7
|
+
event :ignite do
|
8
|
+
transition :stalled => same, :parked => :idling
|
9
|
+
end
|
10
|
+
|
11
|
+
event :idle do
|
12
|
+
transition :first_gear => :idling
|
13
|
+
end
|
14
|
+
|
15
|
+
event :shift_up do
|
16
|
+
transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
|
17
|
+
end
|
18
|
+
|
19
|
+
event :shift_down do
|
20
|
+
transition :third_gear => :second_gear, :second_gear => :first_gear
|
21
|
+
end
|
22
|
+
|
23
|
+
event :crash do
|
24
|
+
transition [:first_gear, :second_gear, :third_gear] => :stalled
|
25
|
+
end
|
26
|
+
|
27
|
+
event :repair do
|
28
|
+
transition :stalled => :parked
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'state_machine'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Provides a set of helper methods for making assertions about the content
|
3
|
+
# of various objects
|
4
|
+
module Assertions
|
5
|
+
# Validates that the given hash *only* includes the specified valid keys.
|
6
|
+
# If any invalid keys are found, an ArgumentError will be raised.
|
7
|
+
#
|
8
|
+
# == Examples
|
9
|
+
#
|
10
|
+
# options = {:name => 'John Smith', :age => 30}
|
11
|
+
#
|
12
|
+
# assert_valid_keys(options, :name) # => ArgumentError: Invalid key(s): age
|
13
|
+
# assert_valid_keys(options, 'name', 'age') # => ArgumentError: Invalid key(s): age, name
|
14
|
+
# assert_valid_keys(options, :name, :age) # => nil
|
15
|
+
def assert_valid_keys(hash, *valid_keys)
|
16
|
+
invalid_keys = hash.keys - valid_keys
|
17
|
+
raise ArgumentError, "Invalid key(s): #{invalid_keys.join(', ')}" unless invalid_keys.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Validates that the given hash only includes at *most* one of a set of
|
21
|
+
# exclusive keys. If more than one key is found, an ArgumentError will be
|
22
|
+
# raised.
|
23
|
+
#
|
24
|
+
# == Examples
|
25
|
+
#
|
26
|
+
# options = {:only => :on, :except => :off}
|
27
|
+
# assert_exclusive_keys(options, :only) # => nil
|
28
|
+
# assert_exclusive_keys(options, :except) # => nil
|
29
|
+
# assert_exclusive_keys(options, :only, :except) # => ArgumentError: Conflicting keys: only, except
|
30
|
+
# assert_exclusive_keys(options, :only, :except, :with) # => ArgumentError: Conflicting keys: only, except
|
31
|
+
def assert_exclusive_keys(hash, *exclusive_keys)
|
32
|
+
conflicting_keys = exclusive_keys & hash.keys
|
33
|
+
raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}" unless conflicting_keys.length <= 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'state_machine/guard'
|
2
|
+
require 'state_machine/eval_helpers'
|
3
|
+
|
4
|
+
module StateMachine
|
5
|
+
# Callbacks represent hooks into objects that allow logic to be triggered
|
6
|
+
# before, after, or around a specific set of transitions.
|
7
|
+
class Callback
|
8
|
+
include EvalHelpers
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Determines whether to automatically bind the callback to the object
|
12
|
+
# being transitioned. This only applies to callbacks that are defined as
|
13
|
+
# lambda blocks (or Procs). Some integrations, such as DataMapper, handle
|
14
|
+
# callbacks by executing them bound to the object involved, while other
|
15
|
+
# integrations, such as ActiveRecord, pass the object as an argument to
|
16
|
+
# the callback. This can be configured on an application-wide basis by
|
17
|
+
# setting this configuration to +true+ or +false+. The default value
|
18
|
+
# is +false+.
|
19
|
+
#
|
20
|
+
# *Note* that the DataMapper and Sequel integrations automatically
|
21
|
+
# configure this value on a per-callback basis, so it does not have to
|
22
|
+
# be enabled application-wide.
|
23
|
+
#
|
24
|
+
# == Examples
|
25
|
+
#
|
26
|
+
# When not bound to the object:
|
27
|
+
#
|
28
|
+
# class Vehicle
|
29
|
+
# state_machine do
|
30
|
+
# before_transition do |vehicle|
|
31
|
+
# vehicle.set_alarm
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def set_alarm
|
36
|
+
# ...
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# When bound to the object:
|
41
|
+
#
|
42
|
+
# StateMachine::Callback.bind_to_object = true
|
43
|
+
#
|
44
|
+
# class Vehicle
|
45
|
+
# state_machine do
|
46
|
+
# before_transition do
|
47
|
+
# self.set_alarm
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def set_alarm
|
52
|
+
# ...
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
attr_accessor :bind_to_object
|
56
|
+
|
57
|
+
# The application-wide terminator to use for callbacks when not
|
58
|
+
# explicitly defined. Terminators determine whether to cancel a
|
59
|
+
# callback chain based on the return value of the callback.
|
60
|
+
#
|
61
|
+
# See StateMachine::Callback#terminator for more information.
|
62
|
+
attr_accessor :terminator
|
63
|
+
end
|
64
|
+
|
65
|
+
# The type of callback chain this callback is for. This can be one of the
|
66
|
+
# following:
|
67
|
+
# * +before+
|
68
|
+
# * +after+
|
69
|
+
# * +around+
|
70
|
+
attr_accessor :type
|
71
|
+
|
72
|
+
# An optional block for determining whether to cancel the callback chain
|
73
|
+
# based on the return value of the callback. By default, the callback
|
74
|
+
# chain never cancels based on the return value (i.e. there is no implicit
|
75
|
+
# terminator). Certain integrations, such as ActiveRecord and Sequel,
|
76
|
+
# change this default value.
|
77
|
+
#
|
78
|
+
# == Examples
|
79
|
+
#
|
80
|
+
# Canceling the callback chain without a terminator:
|
81
|
+
#
|
82
|
+
# class Vehicle
|
83
|
+
# state_machine do
|
84
|
+
# before_transition do |vehicle|
|
85
|
+
# throw :halt
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# Canceling the callback chain with a terminator value of +false+:
|
91
|
+
#
|
92
|
+
# class Vehicle
|
93
|
+
# state_machine do
|
94
|
+
# before_transition do |vehicle|
|
95
|
+
# false
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
attr_reader :terminator
|
100
|
+
|
101
|
+
# The guard that determines whether or not this callback can be invoked
|
102
|
+
# based on the context of the transition. The event, from state, and
|
103
|
+
# to state must all match in order for the guard to pass.
|
104
|
+
#
|
105
|
+
# See StateMachine::Guard for more information.
|
106
|
+
attr_reader :guard
|
107
|
+
|
108
|
+
# Creates a new callback that can get called based on the configured
|
109
|
+
# options.
|
110
|
+
#
|
111
|
+
# In addition to the possible configuration options for guards, the
|
112
|
+
# following options can be configured:
|
113
|
+
# * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
|
114
|
+
# If set to false, the object will be passed as a parameter instead.
|
115
|
+
# Default is integration-specific or set to the application default.
|
116
|
+
# * <tt>:terminator</tt> - A block/proc that determines what callback
|
117
|
+
# results should cause the callback chain to halt (if not using the
|
118
|
+
# default <tt>throw :halt</tt> technique).
|
119
|
+
#
|
120
|
+
# More information about how those options affect the behavior of the
|
121
|
+
# callback can be found in their attribute definitions.
|
122
|
+
def initialize(type, *args, &block)
|
123
|
+
@type = type
|
124
|
+
raise ArgumentError, 'Type must be :before, :after, or :around' unless [:before, :after, :around].include?(type)
|
125
|
+
|
126
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
127
|
+
@methods = args
|
128
|
+
@methods.concat(Array(options.delete(:do)))
|
129
|
+
@methods << block if block_given?
|
130
|
+
raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any?
|
131
|
+
|
132
|
+
options = {:bind_to_object => self.class.bind_to_object, :terminator => self.class.terminator}.merge(options)
|
133
|
+
|
134
|
+
# Proxy lambda blocks so that they're bound to the object
|
135
|
+
bind_to_object = options.delete(:bind_to_object)
|
136
|
+
@methods.map! do |method|
|
137
|
+
bind_to_object && method.is_a?(Proc) ? bound_method(method) : method
|
138
|
+
end
|
139
|
+
|
140
|
+
@terminator = options.delete(:terminator)
|
141
|
+
@guard = Guard.new(options)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Gets a list of the states known to this callback by looking at the
|
145
|
+
# guard's known states
|
146
|
+
def known_states
|
147
|
+
guard.known_states
|
148
|
+
end
|
149
|
+
|
150
|
+
# Runs the callback as long as the transition context matches the guard
|
151
|
+
# requirements configured for this callback. If a block is provided, it
|
152
|
+
# will be called when the last method has run.
|
153
|
+
#
|
154
|
+
# If a terminator has been configured and it matches the result from the
|
155
|
+
# evaluated method, then the callback chain should be halted.
|
156
|
+
def call(object, context = {}, *args, &block)
|
157
|
+
if @guard.matches?(object, context)
|
158
|
+
run_methods(object, context, 0, *args, &block)
|
159
|
+
true
|
160
|
+
else
|
161
|
+
false
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Verifies that the success requirement for this callback matches the given
|
166
|
+
# value
|
167
|
+
def matches_success?(success)
|
168
|
+
guard.success_requirement.matches?(success)
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
# Runs all of the methods configured for this callback.
|
173
|
+
#
|
174
|
+
# When running +around+ callbacks, this will evaluate each method and
|
175
|
+
# yield when the last method has yielded. The callback will only halt if
|
176
|
+
# one of the methods does not yield.
|
177
|
+
#
|
178
|
+
# For all other types of callbacks, this will evaluate each method in
|
179
|
+
# order. The callback will only halt if the resulting value from the
|
180
|
+
# method passes the terminator.
|
181
|
+
def run_methods(object, context = {}, index = 0, *args, &block)
|
182
|
+
if type == :around
|
183
|
+
if method = @methods[index]
|
184
|
+
yielded = false
|
185
|
+
evaluate_method(object, method, *args) do
|
186
|
+
yielded = true
|
187
|
+
run_methods(object, context, index + 1, *args, &block)
|
188
|
+
end
|
189
|
+
|
190
|
+
throw :halt unless yielded
|
191
|
+
else
|
192
|
+
yield if block_given?
|
193
|
+
end
|
194
|
+
else
|
195
|
+
@methods.each do |method|
|
196
|
+
result = evaluate_method(object, method, *args)
|
197
|
+
throw :halt if @terminator && @terminator.call(result)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Generates a method that can be bound to the object being transitioned
|
203
|
+
# when the callback is invoked
|
204
|
+
def bound_method(block)
|
205
|
+
type = self.type
|
206
|
+
arity = block.arity
|
207
|
+
arity += 1 if arity >= 0 # Make sure the object gets passed
|
208
|
+
arity += 1 if arity == 1 && type == :around # Make sure the block gets passed
|
209
|
+
|
210
|
+
method = if RUBY_VERSION >= '1.9'
|
211
|
+
lambda do |object, *args|
|
212
|
+
object.instance_exec(*args, &block)
|
213
|
+
end
|
214
|
+
else
|
215
|
+
# Generate a thread-safe unbound method that can be used on any object.
|
216
|
+
# This is a workaround for not having Ruby 1.9's instance_exec
|
217
|
+
unbound_method = Object.class_eval do
|
218
|
+
time = Time.now
|
219
|
+
method_name = "__bind_#{time.to_i}_#{time.usec}"
|
220
|
+
define_method(method_name, &block)
|
221
|
+
method = instance_method(method_name)
|
222
|
+
remove_method(method_name)
|
223
|
+
method
|
224
|
+
end
|
225
|
+
|
226
|
+
# Proxy calls to the method so that the method can be bound *and*
|
227
|
+
# the arguments are adjusted
|
228
|
+
lambda do |object, *args|
|
229
|
+
unbound_method.bind(object).call(*args)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Proxy arity to the original block
|
234
|
+
(class << method; self; end).class_eval do
|
235
|
+
define_method(:arity) { arity }
|
236
|
+
end
|
237
|
+
|
238
|
+
method
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|