simple_state_machine 0.4.3 → 0.5.0.beta

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ #--debugger
3
+ --backtrace
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # A sample Gemfile
2
+ source "http://rubygems.org"
3
+
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simple_state_machine (1.5.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ ZenTest (4.4.2)
10
+ activerecord (2.3.10)
11
+ activesupport (= 2.3.10)
12
+ activesupport (2.3.10)
13
+ diff-lcs (1.1.2)
14
+ rake (0.8.7)
15
+ rspec (2.5.0)
16
+ rspec-core (~> 2.5.0)
17
+ rspec-expectations (~> 2.5.0)
18
+ rspec-mocks (~> 2.5.0)
19
+ rspec-core (2.5.1)
20
+ rspec-expectations (2.5.0)
21
+ diff-lcs (~> 1.1.2)
22
+ rspec-mocks (2.5.0)
23
+ sqlite3-ruby (1.3.1)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ ZenTest
30
+ activerecord (~> 2.3.5)
31
+ rake
32
+ rspec
33
+ simple_state_machine!
34
+ sqlite3-ruby
data/Rakefile CHANGED
@@ -1,38 +1,21 @@
1
- require 'rubygems'
2
- require 'rake'
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
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/mdh/ssm"
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
4
+ #require 'spec/rake/spectask'
5
+ #Spec::Rake::SpecTask.new(:spec) do |spec|
6
+ # spec.libs << 'lib' << 'spec'
7
+ # spec.spec_files = FileList['spec/**/*_spec.rb']
8
+ #end
9
+ #
10
+ #Spec::Rake::SpecTask.new(:rcov) do |spec|
11
+ # spec.libs << 'lib' << 'spec'
12
+ # spec.pattern = 'spec/**/*_spec.rb'
13
+ # spec.rcov = true
14
+ #end
15
+ #
16
+ #task :spec => :check_dependencies
17
+ #
18
+ #task :default => :spec
36
19
 
37
20
  require 'rake/rdoctask'
38
21
  Rake::RDocTask.new do |rdoc|
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
@@ -1,10 +1,8 @@
1
1
  module SimpleStateMachine::ActiveRecord
2
2
 
3
- include SimpleStateMachine::StateMachineMixin
4
-
5
- def state_machine_decorator subject
6
- Decorator.new subject
7
- end
3
+ include SimpleStateMachine::Mountable
4
+ include SimpleStateMachine::Extendable
5
+ include SimpleStateMachine::Inheritable
8
6
 
9
7
  class Decorator < SimpleStateMachine::Decorator
10
8
 
@@ -60,5 +58,12 @@ module SimpleStateMachine::ActiveRecord
60
58
  def define_state_getter_method; end
61
59
 
62
60
  end
63
-
61
+
62
+ def state_machine_definition
63
+ unless @state_machine_definition
64
+ @state_machine_definition = SimpleStateMachine::StateMachineDefinition.new
65
+ @state_machine_definition.lazy_decorator = lambda { Decorator.new(self) }
66
+ end
67
+ @state_machine_definition
68
+ end
64
69
  end
@@ -1,50 +1,68 @@
1
1
  module SimpleStateMachine
2
+
3
+ require 'cgi'
2
4
 
3
- class Error < ::RuntimeError
5
+ class IllegalStateTransitionError < ::RuntimeError
4
6
  end
5
7
 
8
+ ##
9
+ # Allows class to mount a state_machine
10
+ module Mountable
11
+ def state_machine_definition
12
+ unless @state_machine_definition
13
+ @state_machine_definition = StateMachineDefinition.new
14
+ @state_machine_definition.lazy_decorator = lambda { Decorator.new(self) }
15
+ end
16
+ @state_machine_definition
17
+ end
18
+
19
+ def state_machine_definition= state_machine_definition
20
+ @state_machine_definition = state_machine_definition
21
+ state_machine_definition.transitions.each do |transition|
22
+ state_machine_definition.decorator.decorate(transition)
23
+ end
24
+ end
25
+ end
26
+ include Mountable
27
+
6
28
  ##
7
29
  # Adds state machine methods to the extended class
8
- module StateMachineMixin
30
+ module Extendable
9
31
 
10
32
  # mark the method as an event and specify how the state should transition
11
33
  def event event_name, state_transitions
12
34
  state_transitions.each do |froms, to|
13
35
  [froms].flatten.each do |from|
14
- transition = state_machine_definition.add_transition(event_name, from, to)
15
- state_machine_decorator(self).decorate(transition)
36
+ state_machine_definition.add_transition(event_name, from, to)
16
37
  end
17
38
  end
18
39
  end
19
40
 
20
- def state_machine_definition
21
- @state_machine_definition ||= StateMachineDefinition.new
22
- end
23
-
24
- def state_machine_definition= state_machine_definition
25
- @state_machine_definition = state_machine_definition
26
- state_machine_definition.transitions.each do |transition|
27
- state_machine_decorator(self).decorate(transition)
28
- end
29
- end
30
-
31
- def state_machine_decorator subject
32
- Decorator.new subject
33
- end
41
+ end
42
+ include Extendable
34
43
 
44
+ ##
45
+ # Allows subclasses to inherit state machines
46
+ module Inheritable
35
47
  def inherited(subclass)
36
48
  subclass.state_machine_definition = state_machine_definition.clone
49
+ decorator = state_machine_definition.decorator
50
+ decorator.subject = subclass
51
+ subclass.state_machine_definition.decorator = decorator
37
52
  super
38
53
  end
39
54
  end
40
-
41
- include StateMachineMixin
55
+ include Inheritable
42
56
 
43
57
  ##
44
58
  # Defines state machine transitions
45
59
  class StateMachineDefinition
46
60
 
47
- attr_writer :state_method
61
+ attr_writer :state_method, :decorator, :lazy_decorator
62
+
63
+ def decorator
64
+ @decorator ||= @lazy_decorator.call
65
+ end
48
66
 
49
67
  def transitions
50
68
  @transitions ||= []
@@ -53,12 +71,28 @@ module SimpleStateMachine
53
71
  def add_transition event_name, from, to
54
72
  transition = Transition.new(event_name, from, to)
55
73
  transitions << transition
56
- transition
74
+ decorator.decorate(transition)
57
75
  end
58
76
 
59
77
  def state_method
60
78
  @state_method ||= :state
61
79
  end
80
+
81
+ # Human readable format: old_state.event! => new_state
82
+ def to_s
83
+ transitions.map(&:to_s).join("\n")
84
+ end
85
+
86
+ # Graphiz dot format for rendering as a directional graph
87
+ def to_graphiz_dot
88
+ transitions.map { |t| t.to_graphiz_dot }.join(";")
89
+ end
90
+
91
+ # Generates a url that renders states and events as a directional graph.
92
+ # See http://code.google.com/apis/chart/docs/gallery/graphviz.html
93
+ def google_chart_url
94
+ "http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape to_graphiz_dot}}"
95
+ end
62
96
  end
63
97
 
64
98
  ##
@@ -128,7 +162,7 @@ module SimpleStateMachine
128
162
 
129
163
  # override with your own implementation, like setting errors in your model
130
164
  def illegal_event_callback event_name
131
- raise Error.new("You cannot '#{event_name}' when state is '#{@subject.send(state_method)}'")
165
+ raise IllegalStateTransitionError.new("You cannot '#{event_name}' when state is '#{@subject.send(state_method)}'")
132
166
  end
133
167
 
134
168
  end
@@ -153,6 +187,15 @@ module SimpleStateMachine
153
187
  is_same_event?(event_name) && error.class == from
154
188
  end
155
189
 
190
+ def to_s
191
+ "#{from}.#{event_name}! => #{to}"
192
+ end
193
+
194
+ def to_graphiz_dot
195
+ %("#{from}"->"#{to}"[label=#{event_name}])
196
+ end
197
+
198
+
156
199
  private
157
200
 
158
201
  def is_same_event?(event_name)
@@ -168,6 +211,7 @@ module SimpleStateMachine
168
211
  # Decorates @subject with methods to access the state machine
169
212
  class Decorator
170
213
 
214
+ attr_writer :subject
171
215
  def initialize(subject)
172
216
  @subject = subject
173
217
  define_state_machine_method
@@ -191,7 +235,7 @@ module SimpleStateMachine
191
235
  end
192
236
 
193
237
  def define_state_helper_method state
194
- unless @subject.method_defined?("#{state.to_s}?")
238
+ unless any_method_defined?("#{state.to_s}?")
195
239
  @subject.send(:define_method, "#{state.to_s}?") do
196
240
  self.send(self.class.state_machine_definition.state_method) == state.to_s
197
241
  end
@@ -199,7 +243,7 @@ module SimpleStateMachine
199
243
  end
200
244
 
201
245
  def define_event_method event_name
202
- unless @subject.method_defined?("#{event_name}")
246
+ unless any_method_defined?("#{event_name}")
203
247
  @subject.send(:define_method, "#{event_name}") {}
204
248
  end
205
249
  end
@@ -218,7 +262,7 @@ module SimpleStateMachine
218
262
  end
219
263
 
220
264
  def define_state_setter_method
221
- unless @subject.method_defined?("#{state_method}=")
265
+ unless any_method_defined?("#{state_method}=")
222
266
  @subject.send(:define_method, "#{state_method}=") do |new_state|
223
267
  instance_variable_set(:"@#{self.class.state_machine_definition.state_method}", new_state)
224
268
  end
@@ -226,10 +270,16 @@ module SimpleStateMachine
226
270
  end
227
271
 
228
272
  def define_state_getter_method
229
- unless @subject.method_defined?(state_method)
273
+ unless any_method_defined?(state_method)
230
274
  @subject.send(:attr_reader, state_method)
231
275
  end
232
276
  end
277
+
278
+ def any_method_defined?(method)
279
+ @subject.method_defined?(method) ||
280
+ @subject.protected_method_defined?(method) ||
281
+ @subject.private_method_defined?(method)
282
+ end
233
283
 
234
284
  protected
235
285
 
@@ -0,0 +1,3 @@
1
+ module SimpleStateMachine
2
+ VERSION = "0.5.0.beta"
3
+ end
@@ -1,2 +1,2 @@
1
1
  require 'simple_state_machine/simple_state_machine'
2
- require 'simple_state_machine/active_record'
2
+ require 'simple_state_machine/active_record'
@@ -1,72 +1,31 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "simple_state_machine/version"
5
4
 
6
5
  Gem::Specification.new do |s|
7
6
  s.name = %q{simple_state_machine}
8
- s.version = "0.4.3"
7
+ s.version = SimpleStateMachine::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
10
  s.authors = ["Marek de Heus", "Petrik de Heus"]
12
- s.date = %q{2010-09-14}
13
11
  s.description = %q{A simple DSL to decorate existing methods with logic that guards state transitions.}
14
12
  s.email = ["FIX@example.com"]
13
+ s.homepage = %q{http://github.com/mdh/ssm}
15
14
  s.extra_rdoc_files = [
16
15
  "LICENSE",
17
16
  "README.rdoc"
18
17
  ]
19
- s.files = [
20
- ".gitignore",
21
- "LICENSE",
22
- "README.rdoc",
23
- "Rakefile",
24
- "VERSION",
25
- "examples/conversation.rb",
26
- "examples/lamp.rb",
27
- "examples/relationship.rb",
28
- "examples/traffic_light.rb",
29
- "examples/user.rb",
30
- "lib/simple_state_machine.rb",
31
- "lib/simple_state_machine/active_record.rb",
32
- "lib/simple_state_machine/simple_state_machine.rb",
33
- "simple_state_machine.gemspec",
34
- "spec/active_record_spec.rb",
35
- "spec/decorator_spec.rb",
36
- "spec/examples_spec.rb",
37
- "spec/simple_state_machine_spec.rb",
38
- "spec/spec.opts",
39
- "spec/spec_helper.rb"
40
- ]
41
- s.homepage = %q{http://github.com/mdh/ssm}
18
+ s.files = `git ls-files`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
42
20
  s.rdoc_options = ["--charset=UTF-8"]
43
21
  s.require_paths = ["lib"]
44
22
  s.rubygems_version = %q{1.3.7}
45
23
  s.summary = %q{A statemachine that focuses on events instead of states}
46
- s.test_files = [
47
- "spec/active_record_spec.rb",
48
- "spec/decorator_spec.rb",
49
- "spec/examples_spec.rb",
50
- "spec/simple_state_machine_spec.rb",
51
- "spec/spec_helper.rb",
52
- "examples/conversation.rb",
53
- "examples/lamp.rb",
54
- "examples/relationship.rb",
55
- "examples/traffic_light.rb",
56
- "examples/user.rb"
57
- ]
58
-
59
- if s.respond_to? :specification_version then
60
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
- s.specification_version = 3
62
-
63
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
64
- s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
65
- else
66
- s.add_dependency(%q<rspec>, [">= 1.2.9"])
67
- end
68
- else
69
- s.add_dependency(%q<rspec>, [">= 1.2.9"])
70
- end
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.add_development_dependency "rake"
26
+ s.add_development_dependency "ZenTest"
27
+ s.add_development_dependency "rspec"
28
+ s.add_development_dependency "activerecord", "~>2.3.5"
29
+ s.add_development_dependency "sqlite3-ruby"
71
30
  end
72
31
 
@@ -1,28 +1,29 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- require 'rubygems'
4
- gem 'activerecord', '~> 2.3.5'
3
+ require "rubygems"
4
+ require "bundler"
5
+ Bundler.require
6
+ #Bundler.setup(:test)#, :activerecord)
5
7
  require 'active_record'
6
8
  require 'examples/user'
7
9
 
8
- ActiveRecord::Base.logger = Logger.new STDOUT
9
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
10
+ ActiveRecord::Base.logger = Logger.new "test.log"
11
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
12
+ :database => ":memory:")
10
13
 
11
14
  def setup_db
12
15
  ActiveRecord::Schema.define(:version => 1) do
13
16
  create_table :users do |t|
14
- t.column :id, :integer
15
- t.column :name, :string
16
- t.column :state, :string
17
- t.column :activation_code, :string
18
- t.column :created_at, :datetime
19
- t.column :updated_at, :datetime
17
+ t.column :id, :integer
18
+ t.column :name, :string
19
+ t.column :state, :string
20
+ t.column :activation_code, :string
21
+ t.column :created_at, :datetime
22
+ t.column :updated_at, :datetime
20
23
  end
21
- end
22
- ActiveRecord::Schema.define(:version => 1) do
23
24
  create_table :tickets do |t|
24
- t.column :id, :integer
25
- t.column :ssm_state, :string
25
+ t.column :id, :integer
26
+ t.column :ssm_state, :string
26
27
  end
27
28
  end
28
29
  end
@@ -87,8 +88,10 @@ describe ActiveRecord do
87
88
 
88
89
  it "raises an error if an invalid state_transition is called" do
89
90
  user = User.create!(:name => 'name')
90
- l = lambda { user.confirm_invitation_and_save 'abc' }
91
- l.should raise_error(SimpleStateMachine::Error, "You cannot 'confirm_invitation' when state is 'new'")
91
+ expect {
92
+ user.confirm_invitation_and_save 'abc'
93
+ }.to raise_error(SimpleStateMachine::IllegalStateTransitionError,
94
+ "You cannot 'confirm_invitation' when state is 'new'")
92
95
  end
93
96
 
94
97
  it "returns false and keeps state if record is invalid" do
@@ -130,16 +133,20 @@ describe ActiveRecord do
130
133
 
131
134
  it "raises an error if an invalid state_transition is called" do
132
135
  user = User.create!(:name => 'name')
133
- l = lambda { user.confirm_invitation_and_save! 'abc' }
134
- l.should raise_error(SimpleStateMachine::Error, "You cannot 'confirm_invitation' when state is 'new'")
136
+ expect {
137
+ user.confirm_invitation_and_save! 'abc'
138
+ }.to raise_error(SimpleStateMachine::IllegalStateTransitionError,
139
+ "You cannot 'confirm_invitation' when state is 'new'")
135
140
  end
136
141
 
137
142
  it "raises a RecordInvalid and keeps state if record is invalid" do
138
143
  user = User.new
139
144
  user.should be_new
140
145
  user.should_not be_valid
141
- l = lambda { user.invite_and_save! }
142
- l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank")
146
+ expect {
147
+ user.invite_and_save!
148
+ }.to raise_error(ActiveRecord::RecordInvalid,
149
+ "Validation failed: Name can't be blank")
143
150
  user.should be_new
144
151
  end
145
152
 
@@ -147,13 +154,34 @@ describe ActiveRecord do
147
154
  user = User.create!(:name => 'name')
148
155
  user.invite_and_save!
149
156
  user.should be_invited
150
- l = lambda { user.confirm_invitation_and_save!('x') }
151
- l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid")
157
+ expect {
158
+ user.confirm_invitation_and_save!('x')
159
+ }.to raise_error(ActiveRecord::RecordInvalid,
160
+ "Validation failed: Activation code is invalid")
152
161
  user.should be_invited
153
162
  end
154
163
 
155
164
  end
156
165
 
166
+ describe "event" do
167
+
168
+ it "does not persist transitions" do
169
+ user = User.create!(:name => 'name')
170
+ user.invite.should == true
171
+ User.find(user.id).should_not be_invited
172
+ User.find(user.id).activation_code.should be_nil
173
+ end
174
+
175
+ it "returns false and keeps state if record is invalid" do
176
+ user = User.new
177
+ user.should be_new
178
+ user.should_not be_valid
179
+ user.invite.should == false
180
+ user.should be_new
181
+ end
182
+
183
+ end
184
+
157
185
  describe "event!" do
158
186
 
159
187
  it "persists transitions" do
@@ -167,8 +195,7 @@ describe ActiveRecord do
167
195
  user = User.new
168
196
  user.should be_new
169
197
  user.should_not be_valid
170
- l = lambda { user.invite! }
171
- l.should raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank")
198
+ expect { user.invite! }.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't be blank")
172
199
  user.should be_new
173
200
  end
174
201
 
@@ -191,4 +218,6 @@ describe ActiveRecord do
191
218
  end
192
219
 
193
220
  end
221
+
194
222
  end
223
+