simple_state_machine 0.0.0
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/LICENSE +20 -0
- data/README.rdoc +73 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/examples/conversation.rb +34 -0
- data/examples/lamp.rb +21 -0
- data/examples/relationship.rb +87 -0
- data/examples/traffic_light.rb +17 -0
- data/examples/user.rb +37 -0
- data/lib/simple_state_machine.rb +1 -0
- data/lib/simple_state_machine/simple_state_machine.rb +209 -0
- data/simple_state_machine.gemspec +70 -0
- data/spec/active_record_spec.rb +128 -0
- data/spec/decorator_spec.rb +44 -0
- data/spec/examples_spec.rb +59 -0
- data/spec/simple_state_machine_spec.rb +52 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +13 -0
- metadata +111 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Marek de Heus & Petrik de Heus
|
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,73 @@
|
|
1
|
+
= SimpleStateMachine
|
2
|
+
|
3
|
+
A Ruby Statemachine that focuses on events instead of states.
|
4
|
+
|
5
|
+
A state machine should help you:
|
6
|
+
- Encapsulate state,
|
7
|
+
- Guard state transitions,
|
8
|
+
- Make it trivial to add state transitions to any method.
|
9
|
+
|
10
|
+
Instead of using a DSL to define events, we use a different approach.
|
11
|
+
We have a very simple DSL to decorate existing methods with logic that guards
|
12
|
+
state transitions.
|
13
|
+
|
14
|
+
==== example
|
15
|
+
|
16
|
+
def activate_account(activation_code)
|
17
|
+
if activation_code_invalid?(activation_code)
|
18
|
+
errors.add(:activation_code, 'Invalid')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
event :activate_account, :pending => :active
|
22
|
+
|
23
|
+
|
24
|
+
This has a couple of advantages:
|
25
|
+
- Arguments can be passed to 'events',
|
26
|
+
- 'events' can return a value, remember, it's just a method,
|
27
|
+
- Validation errors can be set on the model if you are using ActiveRecord / ActiveModel,
|
28
|
+
- Encapsulate state transitions (no need for :guards).
|
29
|
+
|
30
|
+
To use the code, you need to do 3 things:
|
31
|
+
- extend SimpleStateMachine,
|
32
|
+
- set the initial state,
|
33
|
+
- decorate a method with the event DSL
|
34
|
+
|
35
|
+
==== Example usage
|
36
|
+
|
37
|
+
class Lamp
|
38
|
+
|
39
|
+
extend SimpleStateMachine
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
self.state = :off
|
43
|
+
end
|
44
|
+
|
45
|
+
def push_button1
|
46
|
+
puts "click1: #{state}"
|
47
|
+
end
|
48
|
+
event :push_button1, :off => :on,
|
49
|
+
:on => :off
|
50
|
+
|
51
|
+
def push_button2
|
52
|
+
puts "click1: #{state}"
|
53
|
+
end
|
54
|
+
event :push_button2, :off => :on,
|
55
|
+
:on => :off
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
This code was just released, we do not claim it to be stable.
|
60
|
+
|
61
|
+
== Note on Patches/Pull Requests
|
62
|
+
|
63
|
+
* Fork the project.
|
64
|
+
* Make your feature addition or bug fix.
|
65
|
+
* Add tests for it. This is important so I don't break it in a
|
66
|
+
future version unintentionally.
|
67
|
+
* Commit, do not mess with rakefile, version, or history.
|
68
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
69
|
+
* Send me a pull request. Bonus points for topic branches.
|
70
|
+
|
71
|
+
== Copyright
|
72
|
+
|
73
|
+
Copyright (c) 2010 Marek & Petrik. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "simple_state_machine"
|
8
|
+
gem.summary = %Q{A statemachine that focuses on events instead of states}
|
9
|
+
gem.description = %Q{A simple DSL to decorate existing methods with logic that guards state transitions.}
|
10
|
+
gem.email = ["FIX@example.com"]
|
11
|
+
gem.homepage = "http://github.com/p8/simple_state_machine"
|
12
|
+
gem.authors = ["Marek de Heus", "Petrik de Heus"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
23
|
+
spec.libs << 'lib' << 'spec'
|
24
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
25
|
+
end
|
26
|
+
|
27
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
30
|
+
spec.rcov = true
|
31
|
+
end
|
32
|
+
|
33
|
+
task :spec => :check_dependencies
|
34
|
+
|
35
|
+
task :default => :spec
|
36
|
+
|
37
|
+
require 'rake/rdoctask'
|
38
|
+
Rake::RDocTask.new do |rdoc|
|
39
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
40
|
+
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = "simple_state_machine #{version}"
|
43
|
+
rdoc.rdoc_files.include('README*')
|
44
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Implementation of the AASM Conversation
|
2
|
+
#
|
3
|
+
# class Conversation
|
4
|
+
# include AASM
|
5
|
+
#
|
6
|
+
# aasm_column :current_state # defaults to aasm_state
|
7
|
+
#
|
8
|
+
# aasm_initial_state :unread
|
9
|
+
#
|
10
|
+
# aasm_state :unread
|
11
|
+
# aasm_state :read
|
12
|
+
# aasm_state :closed
|
13
|
+
#
|
14
|
+
# aasm_event :view do
|
15
|
+
# transitions :to => :read, :from => [:unread]
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# aasm_event :close do
|
19
|
+
# transitions :to => :closed, :from => [:read, :unread]
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
|
23
|
+
class Conversation
|
24
|
+
extend SimpleStateMachine
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
self.state = 'unread'
|
28
|
+
end
|
29
|
+
|
30
|
+
event :view, :unread => :read
|
31
|
+
event :close, :unread => :closed,
|
32
|
+
:read => :closed
|
33
|
+
|
34
|
+
end
|
data/examples/lamp.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class Lamp
|
2
|
+
|
3
|
+
extend SimpleStateMachine
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
self.state = 'off'
|
7
|
+
end
|
8
|
+
|
9
|
+
def push_button1
|
10
|
+
puts "click1: #{state}"
|
11
|
+
end
|
12
|
+
event :push_button1, :off => :on,
|
13
|
+
:on => :off
|
14
|
+
|
15
|
+
def push_button2
|
16
|
+
puts "click2: #{state}"
|
17
|
+
end
|
18
|
+
event :push_button2, :off => :on,
|
19
|
+
:on => :off
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# Implementation of the AASM Conversation
|
2
|
+
#
|
3
|
+
# class Relationship
|
4
|
+
# include AASM
|
5
|
+
#
|
6
|
+
# aasm_column :status
|
7
|
+
#
|
8
|
+
# aasm_initial_state Proc.new { |relationship| relationship.strictly_for_fun? ? :intimate : :dating }
|
9
|
+
#
|
10
|
+
# aasm_state :dating, :enter => :make_happy, :exit => :make_depressed
|
11
|
+
# aasm_state :intimate, :enter => :make_very_happy, :exit => :never_speak_again
|
12
|
+
# aasm_state :married, :enter => :give_up_intimacy, :exit => :buy_exotic_car_and_wear_a_combover
|
13
|
+
#
|
14
|
+
# aasm_event :get_intimate do
|
15
|
+
# transitions :to => :intimate, :from => [:dating], :guard => :drunk?
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# aasm_event :get_married do
|
19
|
+
# transitions :to => :married, :from => [:dating, :intimate], :guard => :willing_to_give_up_manhood?
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def strictly_for_fun?; end
|
23
|
+
# def drunk?; end
|
24
|
+
# def willing_to_give_up_manhood?; end
|
25
|
+
# def make_happy; end
|
26
|
+
# def make_depressed; end
|
27
|
+
# def make_very_happy; end
|
28
|
+
# def never_speak_again; end
|
29
|
+
# def give_up_intimacy; end
|
30
|
+
# def buy_exotic_car_and_wear_a_combover; end
|
31
|
+
# end
|
32
|
+
|
33
|
+
class Relationship
|
34
|
+
extend SimpleStateMachine
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
self.state = relationship.strictly_for_fun? ? get_intimate : start_dating
|
38
|
+
end
|
39
|
+
|
40
|
+
def start_dating
|
41
|
+
make_happy
|
42
|
+
# make_depressed not called on exit
|
43
|
+
end
|
44
|
+
event :start_dating, nil => :dating
|
45
|
+
|
46
|
+
def get_intimate
|
47
|
+
# exit if drunk?
|
48
|
+
make_very_happy
|
49
|
+
# never_speak_again not called on exit
|
50
|
+
end
|
51
|
+
event :get_intimate, :dating => :intimate
|
52
|
+
|
53
|
+
def get_married
|
54
|
+
give_up_intimacy
|
55
|
+
#exit unless willing_to_give_up_manhood?
|
56
|
+
# buy_exotic_car_and_wear_a_combover not called on exit
|
57
|
+
end
|
58
|
+
event :get_married, :dating => :married,
|
59
|
+
:intimate => :married
|
60
|
+
|
61
|
+
# extra event added
|
62
|
+
def stop_dating
|
63
|
+
make_depressed
|
64
|
+
end
|
65
|
+
event :stop_dating, :dating => nil
|
66
|
+
|
67
|
+
def dump
|
68
|
+
never_speak_again
|
69
|
+
end
|
70
|
+
event :dump, :intimate => :dump
|
71
|
+
|
72
|
+
def divorce
|
73
|
+
buy_exotic_car_and_wear_a_combover
|
74
|
+
end
|
75
|
+
event :divorce, :married => :divorced
|
76
|
+
|
77
|
+
|
78
|
+
def strictly_for_fun?; end
|
79
|
+
def drunk?; end
|
80
|
+
def willing_to_give_up_manhood?; end
|
81
|
+
def make_happy; end
|
82
|
+
def make_depressed; end
|
83
|
+
def make_very_happy; end
|
84
|
+
def never_speak_again; end
|
85
|
+
def give_up_intimacy; end
|
86
|
+
def buy_exotic_car_and_wear_a_combover; end
|
87
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class TrafficLight
|
2
|
+
|
3
|
+
extend SimpleStateMachine
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
self.state = 'green'
|
7
|
+
end
|
8
|
+
|
9
|
+
# state machine events
|
10
|
+
def change_state
|
11
|
+
puts "#{state} => #{@next_state}"
|
12
|
+
end
|
13
|
+
|
14
|
+
event :change_state, :green => :orange,
|
15
|
+
:orange => :red,
|
16
|
+
:red => :green
|
17
|
+
end
|
data/examples/user.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
class User < ActiveRecord::Base
|
3
|
+
|
4
|
+
validates_presence_of :name
|
5
|
+
|
6
|
+
extend SimpleStateMachine
|
7
|
+
|
8
|
+
def after_initialize
|
9
|
+
self.state ||= 'new'
|
10
|
+
end
|
11
|
+
|
12
|
+
def invite
|
13
|
+
self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}")
|
14
|
+
send_activation_email(self.activation_code)
|
15
|
+
end
|
16
|
+
event :invite, :new => :invited
|
17
|
+
|
18
|
+
def confirm_invitation activation_code
|
19
|
+
if activation_code != self.activation_code
|
20
|
+
errors.add(:activation_code, 'Invalid')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
event :confirm_invitation, :invited => :active
|
24
|
+
|
25
|
+
#event :log_send_activation_code_failed, :new => :send_activation_code_failed
|
26
|
+
#
|
27
|
+
# def reset_password(new_password)
|
28
|
+
# self.password = new_password
|
29
|
+
# end
|
30
|
+
# # do not change state, but ensure that we are in proper state
|
31
|
+
# event :reset_password, :active => :active
|
32
|
+
|
33
|
+
def send_activation_email(code)
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'simple_state_machine/simple_state_machine'
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module SimpleStateMachine
|
2
|
+
|
3
|
+
def event event_name, state_transitions
|
4
|
+
@state_machine_definition ||= StateMachineDefinition.new self
|
5
|
+
@state_machine_definition.define_event event_name, state_transitions
|
6
|
+
end
|
7
|
+
|
8
|
+
def state_machine_definition
|
9
|
+
@state_machine_definition
|
10
|
+
end
|
11
|
+
|
12
|
+
class StateMachineDefinition
|
13
|
+
|
14
|
+
attr_reader :events
|
15
|
+
|
16
|
+
def initialize subject
|
17
|
+
@events = {}
|
18
|
+
@decorator = if inherits_from_active_record_base?(subject)
|
19
|
+
Decorator::ActiveRecord.new(subject)
|
20
|
+
else
|
21
|
+
Decorator::Base.new(subject)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def define_event event_name, state_transitions
|
26
|
+
@events[event_name.to_s] ||= {}
|
27
|
+
state_transitions.each do |from, to|
|
28
|
+
@events[event_name.to_s][from.to_s] = to.to_s
|
29
|
+
@decorator.decorate(event_name, from, to)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def inherits_from_active_record_base?(subject)
|
34
|
+
subject.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base")
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
module StateMachine
|
40
|
+
|
41
|
+
class Base
|
42
|
+
def initialize(subject)
|
43
|
+
@subject = subject
|
44
|
+
end
|
45
|
+
|
46
|
+
def next_state(event_name)
|
47
|
+
@subject.class.state_machine_definition.events[event_name.to_s][@subject.state]
|
48
|
+
end
|
49
|
+
|
50
|
+
def transition(event_name)
|
51
|
+
if to = next_state(event_name)
|
52
|
+
result = yield
|
53
|
+
@subject.state = to
|
54
|
+
return result
|
55
|
+
else
|
56
|
+
illegal_event_callback event_name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def illegal_event_callback event_name
|
63
|
+
# override with your own implementation, like setting errors in your model
|
64
|
+
raise "You cannot '#{event_name}' when state is '#{@subject.state}'"
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
class ActiveRecord < Base
|
70
|
+
|
71
|
+
def next_state(event_name)
|
72
|
+
if event_name =~ /\!$/
|
73
|
+
event_name = event_name.chop
|
74
|
+
end
|
75
|
+
super event_name
|
76
|
+
end
|
77
|
+
|
78
|
+
def transition(event_name)
|
79
|
+
if to = next_state(event_name)
|
80
|
+
if with_error_counting { yield } > 0 || @subject.invalid?
|
81
|
+
if event_name =~ /\!$/
|
82
|
+
raise ::ActiveRecord::RecordInvalid.new(@subject)
|
83
|
+
else
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
else
|
87
|
+
@subject.state = to
|
88
|
+
if event_name =~ /\!$/
|
89
|
+
@subject.save! #TODO maybe save_without_validation!
|
90
|
+
else
|
91
|
+
@subject.save
|
92
|
+
end
|
93
|
+
end
|
94
|
+
else
|
95
|
+
illegal_event_callback event_name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def with_error_counting
|
102
|
+
original_errors_size = @subject.errors.size
|
103
|
+
yield
|
104
|
+
@subject.errors.size - original_errors_size
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
module Decorator
|
112
|
+
class Base
|
113
|
+
|
114
|
+
def initialize(subject)
|
115
|
+
@subject = subject
|
116
|
+
define_state_machine_method
|
117
|
+
define_state_getter_method
|
118
|
+
define_state_setter_method
|
119
|
+
end
|
120
|
+
|
121
|
+
def decorate event_name, from, to
|
122
|
+
define_state_helper_method(from)
|
123
|
+
define_state_helper_method(to)
|
124
|
+
define_event_method(event_name)
|
125
|
+
decorate_event_method(event_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def define_state_machine_method
|
131
|
+
@subject.send(:define_method, "state_machine") do
|
132
|
+
@state_machine ||= StateMachine::Base.new(self)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def define_state_helper_method state
|
137
|
+
unless @subject.method_defined?("#{state.to_s}?")
|
138
|
+
@subject.send(:define_method, "#{state.to_s}?") do
|
139
|
+
self.state == state.to_s
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def define_event_method event_name
|
145
|
+
unless @subject.method_defined?("#{event_name}")
|
146
|
+
@subject.send(:define_method, "#{event_name}") {}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def decorate_event_method event_name
|
151
|
+
# TODO put in transaction for activeRecord?
|
152
|
+
unless @subject.method_defined?("with_managed_state_#{event_name}")
|
153
|
+
@subject.send(:define_method, "with_managed_state_#{event_name}") do |*args|
|
154
|
+
return state_machine.transition(event_name) do
|
155
|
+
send("without_managed_state_#{event_name}", *args)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
@subject.send :alias_method, "without_managed_state_#{event_name}", event_name
|
159
|
+
@subject.send :alias_method, event_name, "with_managed_state_#{event_name}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def define_state_setter_method
|
164
|
+
unless @subject.method_defined?('state=')
|
165
|
+
@subject.send(:define_method, 'state=') do |new_state|
|
166
|
+
@state = new_state.to_s
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def define_state_getter_method
|
172
|
+
unless @subject.method_defined?('state')
|
173
|
+
@subject.send(:define_method, 'state') do
|
174
|
+
@state
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
class ActiveRecord < Base
|
182
|
+
|
183
|
+
def decorate event_name, from, to
|
184
|
+
super event_name, from, to
|
185
|
+
unless @subject.method_defined?("#{event_name}!")
|
186
|
+
@subject.send(:define_method, "#{event_name}!") do |*args|
|
187
|
+
send "#{event_name}", *args
|
188
|
+
end
|
189
|
+
end
|
190
|
+
decorate_event_method("#{event_name}!")
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def define_state_machine_method
|
196
|
+
@subject.send(:define_method, "state_machine") do
|
197
|
+
@state_machine ||= StateMachine::ActiveRecord.new(self)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def define_state_setter_method; end
|
202
|
+
|
203
|
+
def define_state_getter_method; end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{simple_state_machine}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Marek de Heus", "Petrik de Heus"]
|
12
|
+
s.date = %q{2010-06-28}
|
13
|
+
s.description = %q{A simple DSL to decorate existing methods with logic that guards state transitions.}
|
14
|
+
s.email = ["FIX@example.com"]
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
"LICENSE",
|
21
|
+
"README.rdoc",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"examples/conversation.rb",
|
25
|
+
"examples/lamp.rb",
|
26
|
+
"examples/relationship.rb",
|
27
|
+
"examples/traffic_light.rb",
|
28
|
+
"examples/user.rb",
|
29
|
+
"lib/simple_state_machine.rb",
|
30
|
+
"lib/simple_state_machine/simple_state_machine.rb",
|
31
|
+
"simple_state_machine.gemspec",
|
32
|
+
"spec/active_record_spec.rb",
|
33
|
+
"spec/decorator_spec.rb",
|
34
|
+
"spec/examples_spec.rb",
|
35
|
+
"spec/simple_state_machine_spec.rb",
|
36
|
+
"spec/spec.opts",
|
37
|
+
"spec/spec_helper.rb"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/p8/simple_state_machine}
|
40
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubygems_version = %q{1.3.7}
|
43
|
+
s.summary = %q{A statemachine that focuses on events instead of states}
|
44
|
+
s.test_files = [
|
45
|
+
"spec/active_record_spec.rb",
|
46
|
+
"spec/decorator_spec.rb",
|
47
|
+
"spec/examples_spec.rb",
|
48
|
+
"spec/simple_state_machine_spec.rb",
|
49
|
+
"spec/spec_helper.rb",
|
50
|
+
"examples/conversation.rb",
|
51
|
+
"examples/lamp.rb",
|
52
|
+
"examples/relationship.rb",
|
53
|
+
"examples/traffic_light.rb",
|
54
|
+
"examples/user.rb"
|
55
|
+
]
|
56
|
+
|
57
|
+
if s.respond_to? :specification_version then
|
58
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
59
|
+
s.specification_version = 3
|
60
|
+
|
61
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
62
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
65
|
+
end
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
gem 'activerecord', '>= 1.15.4.7794'
|
5
|
+
require 'active_record'
|
6
|
+
require 'examples/user'
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
9
|
+
|
10
|
+
def setup_db
|
11
|
+
ActiveRecord::Schema.define(:version => 1) do
|
12
|
+
create_table :users do |t|
|
13
|
+
t.column :id, :integer
|
14
|
+
t.column :name, :string
|
15
|
+
t.column :state, :string
|
16
|
+
t.column :activation_code, :string
|
17
|
+
t.column :created_at, :datetime
|
18
|
+
t.column :updated_at, :datetime
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def teardown_db
|
24
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
25
|
+
ActiveRecord::Base.connection.drop_table(table)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe User do
|
30
|
+
|
31
|
+
before do
|
32
|
+
setup_db
|
33
|
+
end
|
34
|
+
|
35
|
+
after do
|
36
|
+
teardown_db
|
37
|
+
end
|
38
|
+
|
39
|
+
it "has a default state" do
|
40
|
+
User.new.should be_new
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "events" do
|
44
|
+
it "persists transitions" do
|
45
|
+
user = User.create!(:name => 'name')
|
46
|
+
user.invite.should == true
|
47
|
+
User.find(user.id).should be_invited
|
48
|
+
User.find(user.id).activation_code.should_not be_nil
|
49
|
+
end
|
50
|
+
|
51
|
+
it "persists transitions with !" do
|
52
|
+
user = User.create!(:name => 'name')
|
53
|
+
user.invite!.should == true
|
54
|
+
User.find(user.id).should be_invited
|
55
|
+
User.find(user.id).activation_code.should_not be_nil
|
56
|
+
end
|
57
|
+
|
58
|
+
it "raises an error if an invalid state_transition is called" do
|
59
|
+
user = User.create!(:name => 'name')
|
60
|
+
l = lambda { user.confirm_invitation 'abc' }
|
61
|
+
l.should raise_error(RuntimeError, "You cannot 'confirm_invitation' when state is 'new'")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns false if event called and record is invalid" do
|
65
|
+
user = User.new
|
66
|
+
user.should_not be_valid
|
67
|
+
user.invite.should == false
|
68
|
+
end
|
69
|
+
|
70
|
+
it "keeps state if event called and record is invalid" do
|
71
|
+
user = User.new
|
72
|
+
user.should_not be_valid
|
73
|
+
user.invite.should == false
|
74
|
+
user.should be_new
|
75
|
+
end
|
76
|
+
|
77
|
+
it "raises a RecordInvalid event if called with ! and record is invalid" do
|
78
|
+
user = User.new
|
79
|
+
user.should_not be_valid
|
80
|
+
l = lambda { user.invite! }
|
81
|
+
l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank")
|
82
|
+
end
|
83
|
+
|
84
|
+
it "keeps state if event! called and record is invalid" do
|
85
|
+
user = User.new
|
86
|
+
user.should_not be_valid
|
87
|
+
begin
|
88
|
+
user.invite!
|
89
|
+
rescue ActiveRecord::RecordInvalid;end
|
90
|
+
user.should be_new
|
91
|
+
end
|
92
|
+
|
93
|
+
it "returns falls if record is valid but event adds errors" do
|
94
|
+
user = User.create!(:name => 'name')
|
95
|
+
user.invite!
|
96
|
+
user.should be_valid
|
97
|
+
r = user.confirm_invitation('x').should == false
|
98
|
+
end
|
99
|
+
|
100
|
+
it "keeps state if record is valid but event adds errors" do
|
101
|
+
user = User.create!(:name => 'name')
|
102
|
+
user.invite!
|
103
|
+
user.should be_valid
|
104
|
+
user.confirm_invitation('x')
|
105
|
+
user.should be_invited
|
106
|
+
end
|
107
|
+
|
108
|
+
it "raises a RecordInvalid if record is valid but event adds errors" do
|
109
|
+
user = User.create!(:name => 'name')
|
110
|
+
user.invite!
|
111
|
+
user.should be_valid
|
112
|
+
l = lambda { user.confirm_invitation!('x') }
|
113
|
+
l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Activation code Invalid")
|
114
|
+
end
|
115
|
+
|
116
|
+
it "keeps state if record is valid but event adds errors" do
|
117
|
+
user = User.create!(:name => 'name')
|
118
|
+
user.invite!
|
119
|
+
user.should be_valid
|
120
|
+
begin
|
121
|
+
user.confirm_invitation!('x')
|
122
|
+
rescue ActiveRecord::RecordInvalid;end
|
123
|
+
user.should be_invited
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
class WithoutEventMethods
|
4
|
+
extend SimpleStateMachine
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.state = 'unread'
|
8
|
+
end
|
9
|
+
event :view, :unread => :read
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class WithPredefinedStateHelperMethods
|
14
|
+
extend SimpleStateMachine
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
self.state = 'unread'
|
18
|
+
end
|
19
|
+
event :view, :unread => :read
|
20
|
+
|
21
|
+
def unread?
|
22
|
+
raise "blah"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
describe SimpleStateMachine::Decorator do
|
28
|
+
|
29
|
+
it "defines state_helper_methods for all states" do
|
30
|
+
TrafficLight.new.green?.should == true
|
31
|
+
TrafficLight.new.orange?.should == false
|
32
|
+
TrafficLight.new.red?.should == false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "does not define an state_helper_method if it already exists" do
|
36
|
+
l = lambda { WithPredefinedStateHelperMethods.new.unread? }
|
37
|
+
l.should raise_error(RuntimeError, 'blah')
|
38
|
+
end
|
39
|
+
|
40
|
+
it "defines an event method if it doesn't exist" do
|
41
|
+
WithoutEventMethods.new.view
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Examples" do
|
4
|
+
describe "TrafficLight" do
|
5
|
+
it "changes to the next state" do
|
6
|
+
tl = TrafficLight.new
|
7
|
+
tl.should be_green
|
8
|
+
tl.change_state
|
9
|
+
tl.should be_orange
|
10
|
+
tl.change_state
|
11
|
+
tl.should be_red
|
12
|
+
tl.change_state
|
13
|
+
tl.should be_green
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "Lamp" do
|
18
|
+
it "changes between :on and :off" do
|
19
|
+
lamp = Lamp.new
|
20
|
+
lamp.should be_off
|
21
|
+
lamp.push_button1
|
22
|
+
lamp.should be_on
|
23
|
+
lamp.push_button2
|
24
|
+
lamp.should be_off
|
25
|
+
lamp.push_button2
|
26
|
+
lamp.should be_on
|
27
|
+
lamp.push_button1
|
28
|
+
lamp.should be_off
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "Conversation" do
|
33
|
+
it "is :unread by default" do
|
34
|
+
conversation = Conversation.new
|
35
|
+
conversation.should be_unread
|
36
|
+
end
|
37
|
+
|
38
|
+
it "changes to read on view" do
|
39
|
+
conversation = Conversation.new
|
40
|
+
conversation.view
|
41
|
+
conversation.should be_read
|
42
|
+
end
|
43
|
+
|
44
|
+
it "changes to closed on close" do
|
45
|
+
conversation = Conversation.new
|
46
|
+
conversation.close
|
47
|
+
conversation.should be_closed
|
48
|
+
end
|
49
|
+
|
50
|
+
it "changes to closed on close if :read" do
|
51
|
+
conversation = Conversation.new
|
52
|
+
conversation.view
|
53
|
+
conversation.close
|
54
|
+
conversation.should be_closed
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
class SimpleExample
|
4
|
+
extend SimpleStateMachine
|
5
|
+
attr_reader :event2_called
|
6
|
+
def initialize
|
7
|
+
self.state = 'state1'
|
8
|
+
end
|
9
|
+
event :event1, :state1 => :state2
|
10
|
+
def event2
|
11
|
+
@event2_called = true
|
12
|
+
'event2'
|
13
|
+
end
|
14
|
+
event :event2, :state2 => :state3
|
15
|
+
end
|
16
|
+
|
17
|
+
describe SimpleStateMachine do
|
18
|
+
|
19
|
+
it "has a default state" do
|
20
|
+
SimpleExample.new.state.should == 'state1'
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "events" do
|
24
|
+
it "raise an error if an invalid state_transition is called" do
|
25
|
+
example = SimpleExample.new
|
26
|
+
lambda { example.event2 }.should raise_error(RuntimeError, "You cannot 'event2' when state is 'state1'")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "return nil" do
|
30
|
+
example = SimpleExample.new
|
31
|
+
example.event1.should == nil
|
32
|
+
example.event2.should == 'event2'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "calls existing methods" do
|
36
|
+
example = SimpleExample.new
|
37
|
+
example.event1
|
38
|
+
example.event2
|
39
|
+
example.event2_called.should == true
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "state_machine" do
|
45
|
+
it "has a next_state method" do
|
46
|
+
example = SimpleExample.new
|
47
|
+
example.state_machine.next_state('event1').should == 'state2'
|
48
|
+
example.state_machine.next_state('event2').should be_nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'simple_state_machine'
|
4
|
+
require 'spec'
|
5
|
+
require 'spec/autorun'
|
6
|
+
|
7
|
+
require 'examples/conversation'
|
8
|
+
require 'examples/lamp'
|
9
|
+
require 'examples/traffic_light'
|
10
|
+
|
11
|
+
Spec::Runner.configure do |config|
|
12
|
+
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple_state_machine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 0.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Marek de Heus
|
14
|
+
- Petrik de Heus
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-06-28 00:00:00 +02:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: rspec
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 13
|
31
|
+
segments:
|
32
|
+
- 1
|
33
|
+
- 2
|
34
|
+
- 9
|
35
|
+
version: 1.2.9
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id001
|
38
|
+
description: A simple DSL to decorate existing methods with logic that guards state transitions.
|
39
|
+
email:
|
40
|
+
- FIX@example.com
|
41
|
+
executables: []
|
42
|
+
|
43
|
+
extensions: []
|
44
|
+
|
45
|
+
extra_rdoc_files:
|
46
|
+
- LICENSE
|
47
|
+
- README.rdoc
|
48
|
+
files:
|
49
|
+
- LICENSE
|
50
|
+
- README.rdoc
|
51
|
+
- Rakefile
|
52
|
+
- VERSION
|
53
|
+
- examples/conversation.rb
|
54
|
+
- examples/lamp.rb
|
55
|
+
- examples/relationship.rb
|
56
|
+
- examples/traffic_light.rb
|
57
|
+
- examples/user.rb
|
58
|
+
- lib/simple_state_machine.rb
|
59
|
+
- lib/simple_state_machine/simple_state_machine.rb
|
60
|
+
- simple_state_machine.gemspec
|
61
|
+
- spec/active_record_spec.rb
|
62
|
+
- spec/decorator_spec.rb
|
63
|
+
- spec/examples_spec.rb
|
64
|
+
- spec/simple_state_machine_spec.rb
|
65
|
+
- spec/spec.opts
|
66
|
+
- spec/spec_helper.rb
|
67
|
+
has_rdoc: true
|
68
|
+
homepage: http://github.com/p8/simple_state_machine
|
69
|
+
licenses: []
|
70
|
+
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options:
|
73
|
+
- --charset=UTF-8
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
hash: 3
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 3
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
version: "0"
|
94
|
+
requirements: []
|
95
|
+
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 1.3.7
|
98
|
+
signing_key:
|
99
|
+
specification_version: 3
|
100
|
+
summary: A statemachine that focuses on events instead of states
|
101
|
+
test_files:
|
102
|
+
- spec/active_record_spec.rb
|
103
|
+
- spec/decorator_spec.rb
|
104
|
+
- spec/examples_spec.rb
|
105
|
+
- spec/simple_state_machine_spec.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- examples/conversation.rb
|
108
|
+
- examples/lamp.rb
|
109
|
+
- examples/relationship.rb
|
110
|
+
- examples/traffic_light.rb
|
111
|
+
- examples/user.rb
|