golem_statemachine 0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +472 -0
- data/Rakefile +23 -0
- data/examples/document.rb +44 -0
- data/examples/monster.rb +100 -0
- data/examples/seminar.rb +138 -0
- data/examples/seminar_enrollment.rb +141 -0
- data/init.rb +5 -0
- data/install.rb +1 -0
- data/lib/golem.rb +207 -0
- data/lib/golem/dsl/decision_def.rb +38 -0
- data/lib/golem/dsl/event_def.rb +89 -0
- data/lib/golem/dsl/state_def.rb +34 -0
- data/lib/golem/dsl/state_machine_def.rb +73 -0
- data/lib/golem/dsl/transition_def.rb +51 -0
- data/lib/golem/model/callback.rb +32 -0
- data/lib/golem/model/condition.rb +12 -0
- data/lib/golem/model/event.rb +12 -0
- data/lib/golem/model/state.rb +26 -0
- data/lib/golem/model/state_machine.rb +224 -0
- data/lib/golem/model/transition.rb +37 -0
- data/lib/golem/util/element_collection.rb +33 -0
- data/tasks/golem_statemachine_tasks.rake +4 -0
- data/test/active_record_test.rb +189 -0
- data/test/dsl_test.rb +429 -0
- data/test/monster_test.rb +110 -0
- data/test/problematic_test.rb +95 -0
- data/test/seminar_test.rb +106 -0
- data/test/statemachine_assertions.rb +79 -0
- data/test/test_helper.rb +5 -0
- data/uninstall.rb +1 -0
- metadata +119 -0
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the golem_statemachine plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate documentation for the golem_statemachine plugin.'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'GolemStatemachine'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__)+"/../lib")
|
2
|
+
require 'golem'
|
3
|
+
|
4
|
+
class Document
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@has_signature = false
|
8
|
+
end
|
9
|
+
|
10
|
+
def sign!
|
11
|
+
@has_signature = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def has_signature?
|
15
|
+
@has_signature
|
16
|
+
end
|
17
|
+
|
18
|
+
include Golem
|
19
|
+
define_statemachine do
|
20
|
+
initial_state :NEW
|
21
|
+
|
22
|
+
state :NEW do
|
23
|
+
on :submit, :to => :SUBMITTED
|
24
|
+
end
|
25
|
+
|
26
|
+
state :SUBMITTED do
|
27
|
+
on :review do
|
28
|
+
transition :to => :APPROVED, :if => :has_signature?
|
29
|
+
transition :to => :REJECTED
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
state :REJECTED do
|
34
|
+
on :revise, :to => :REVISED
|
35
|
+
end
|
36
|
+
|
37
|
+
state :REVISED do
|
38
|
+
on :submit, :to => :SUBMITTED
|
39
|
+
end
|
40
|
+
|
41
|
+
state :APPROVED
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
data/examples/monster.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__)+"/../lib")
|
2
|
+
require 'golem'
|
3
|
+
|
4
|
+
class Monster
|
5
|
+
include Golem
|
6
|
+
|
7
|
+
attr_accessor :deeds
|
8
|
+
attr_accessor :journal
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@deeds = []
|
12
|
+
@journal = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def stretch
|
16
|
+
@deeds << :stretched
|
17
|
+
end
|
18
|
+
|
19
|
+
def grumble
|
20
|
+
@deeds << :grumbled
|
21
|
+
end
|
22
|
+
|
23
|
+
def yawn
|
24
|
+
@deeds << :yawned
|
25
|
+
end
|
26
|
+
|
27
|
+
def barf
|
28
|
+
@deeds << :barfed
|
29
|
+
end
|
30
|
+
|
31
|
+
def likes_food?(food)
|
32
|
+
food == :hamburger
|
33
|
+
end
|
34
|
+
|
35
|
+
def hates_food?(food)
|
36
|
+
food == :tofu
|
37
|
+
end
|
38
|
+
|
39
|
+
def is_tired?
|
40
|
+
deeds.last == :yawned
|
41
|
+
end
|
42
|
+
|
43
|
+
def mouth_is_open?
|
44
|
+
mouth_state == :open
|
45
|
+
end
|
46
|
+
|
47
|
+
def write_in_journal(event, transition, *args)
|
48
|
+
@journal << event.name if event.kind_of?(Golem::Model::Event) && transition.kind_of?(Golem::Model::Transition)
|
49
|
+
end
|
50
|
+
|
51
|
+
# statemachine representing the Monster's affect (i.e. how it is feeling)
|
52
|
+
define_statemachine :affect do
|
53
|
+
initial_state :sleeping
|
54
|
+
on_all_transitions :write_in_journal
|
55
|
+
|
56
|
+
state :sleeping do
|
57
|
+
on :wake_up, :to => :hungry
|
58
|
+
exit :stretch
|
59
|
+
end
|
60
|
+
|
61
|
+
state :hungry do
|
62
|
+
enter :grumble
|
63
|
+
on :feed, :if => :mouth_is_open?, :guard_options => {:failure_message => "its mouth is not open"} do
|
64
|
+
transition :to => :satiated do
|
65
|
+
guard :likes_food?, :failure_message => "it does not like the food"
|
66
|
+
action {|monster,food| monster.deeds << "ate_tasty_#{food}".to_sym}
|
67
|
+
end
|
68
|
+
transition :to => :self do
|
69
|
+
guard :hates_food?, :failure_message => "it does not hate the food"
|
70
|
+
action :barf
|
71
|
+
end
|
72
|
+
transition :to => :self do
|
73
|
+
action {|monster,food| monster.deeds << "ate_#{food}".to_sym}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
on :lullaby, :action => :yawn
|
77
|
+
on :tickle do
|
78
|
+
action {|monster| monster.deeds << :giggled }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
state :satiated do
|
83
|
+
on :lullaby do
|
84
|
+
transition :to => :sleeping, :if => :is_tired?
|
85
|
+
transition :to => :self, :action => :yawn
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# secondary statemachine representing the Monster's mouth
|
91
|
+
define_statemachine :mouth do
|
92
|
+
initial_state :closed
|
93
|
+
state :open do
|
94
|
+
on :lullaby, :to => :closed
|
95
|
+
end
|
96
|
+
state :closed do
|
97
|
+
on :tickle, :to => :open
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/examples/seminar.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__)+"/../lib")
|
2
|
+
require 'golem'
|
3
|
+
|
4
|
+
|
5
|
+
class Seminar
|
6
|
+
attr_accessor :status
|
7
|
+
attr_accessor :students
|
8
|
+
attr_accessor :waiting_list
|
9
|
+
attr_accessor :max_class_size
|
10
|
+
attr_accessor :notifications_sent
|
11
|
+
|
12
|
+
@@out = STDOUT
|
13
|
+
|
14
|
+
def self.output=(output)
|
15
|
+
@@out = output
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@students = [] # list of students enrolled in the course
|
20
|
+
@max_class_size = 5
|
21
|
+
@notifications_sent = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def seats_available
|
25
|
+
@max_class_size - @students.size
|
26
|
+
end
|
27
|
+
|
28
|
+
def waiting_list_is_empty?
|
29
|
+
@waiting_list.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def student_is_enrolled?(student)
|
33
|
+
@students.include? student
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_student_to_waiting_list(student)
|
37
|
+
@waiting_list << student
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_waiting_list
|
41
|
+
@waiting_list = []
|
42
|
+
end
|
43
|
+
|
44
|
+
def notify_waiting_list_that_enrollment_is_closed
|
45
|
+
@waiting_list.each{|student| self.notifications_sent << "#{student}: waiting list is closed"}
|
46
|
+
end
|
47
|
+
|
48
|
+
def notify_students_that_the_seminar_is_cancelled
|
49
|
+
(@students + @waiting_list).each{|student| self.notifications_sent << "#{student}: the seminar has been cancelled"}
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
include Golem
|
54
|
+
|
55
|
+
define_statemachine do
|
56
|
+
initial_state :proposed
|
57
|
+
state_attribute :status
|
58
|
+
|
59
|
+
state :proposed do
|
60
|
+
on :schedule, :to => :scheduled
|
61
|
+
end
|
62
|
+
|
63
|
+
state :scheduled do
|
64
|
+
on :open, :to => :open_for_enrollment
|
65
|
+
end
|
66
|
+
|
67
|
+
state :open_for_enrollment do
|
68
|
+
on :close, :to => :closed_to_enrollment
|
69
|
+
on :enroll_student do
|
70
|
+
transition do
|
71
|
+
guard {|seminar, student| !seminar.student_is_enrolled?(student) && seminar.seats_available > 1 }
|
72
|
+
action {|seminar, student| seminar.students << student}
|
73
|
+
end
|
74
|
+
transition :to => :full do
|
75
|
+
guard {|seminar, student| !seminar.student_is_enrolled?(student) }
|
76
|
+
action do |seminar, student|
|
77
|
+
seminar.create_waiting_list
|
78
|
+
if seminar.seats_available == 1
|
79
|
+
seminar.students << student
|
80
|
+
else
|
81
|
+
seminar.add_student_to_waiting_list(student)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
on :drop_student do
|
87
|
+
transition :if => :student_is_enrolled? do
|
88
|
+
action {|seminar, student| seminar.students.delete student}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
state :full do
|
94
|
+
on :move_to_bigger_classroom, :to => :open_for_enrollment,
|
95
|
+
:action => Proc.new{|seminar, additional_seats| seminar.max_class_size += additional_seats}
|
96
|
+
# Note that this :if condition applies to all transitions inside the event, in addition to each
|
97
|
+
# transaction's own :if/guard statement.
|
98
|
+
on :drop_student, :if => :student_is_enrolled? do
|
99
|
+
transition :to => :open_for_enrollment, :if => :waiting_list_is_empty? do
|
100
|
+
action {|seminar, student| seminar.students.delete student}
|
101
|
+
end
|
102
|
+
transition do
|
103
|
+
action do |seminar, student|
|
104
|
+
seminar.students.delete student
|
105
|
+
seminar.enroll_student seminar.waiting_list.shift
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
on :enroll_student, :if => Proc.new{|seminar, student| !seminar.student_is_enrolled?(student)} do
|
110
|
+
transition do
|
111
|
+
guard {|seminar, student| seminar.seats_available > 0}
|
112
|
+
action {|seminar, student| seminar.students << student}
|
113
|
+
end
|
114
|
+
transition :action => :add_student_to_waiting_list
|
115
|
+
end
|
116
|
+
on :close, :to => :closed_to_enrollment
|
117
|
+
end
|
118
|
+
|
119
|
+
state :closed_to_enrollment do
|
120
|
+
enter :notify_waiting_list_that_enrollment_is_closed
|
121
|
+
end
|
122
|
+
|
123
|
+
state :cancelled do
|
124
|
+
enter :notify_students_that_the_seminar_is_cancelled
|
125
|
+
end
|
126
|
+
|
127
|
+
# The 'cancel' event can occur in all states.
|
128
|
+
all_states.each do |state|
|
129
|
+
state.on :cancel, :to => :cancelled
|
130
|
+
end
|
131
|
+
|
132
|
+
on_all_transitions do |seminar, event, transition, *event_args|
|
133
|
+
@@out.puts "==[#{event.name}(#{event_args.collect{|arg| arg.inspect}.join(",")})]==> #{transition.from.name} --> #{transition.to.name}"
|
134
|
+
@@out.puts " ENROLLED: #{seminar.students.inspect}"
|
135
|
+
@@out.puts " WAITING: #{seminar.waiting_list.inspect}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__)+"/../lib")
|
2
|
+
require 'golem'
|
3
|
+
|
4
|
+
|
5
|
+
class Seminar
|
6
|
+
attr_accessor :status
|
7
|
+
attr_accessor :students
|
8
|
+
attr_accessor :waiting_list
|
9
|
+
attr_accessor :max_size
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@students = [] # list of students enrolled in the course
|
13
|
+
@max_class_size = 5
|
14
|
+
end
|
15
|
+
|
16
|
+
def seats_available?
|
17
|
+
@students.size < @max_class_size
|
18
|
+
end
|
19
|
+
|
20
|
+
def waiting_list_is_empty?
|
21
|
+
@waiting_list.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def student_is_enrolled?(student)
|
25
|
+
@students.include? student
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_student_to_waiting_list(student)
|
29
|
+
@waiting_list << student
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_waiting_list
|
33
|
+
@waiting_list = []
|
34
|
+
end
|
35
|
+
|
36
|
+
def notify_waiting_list_that_enrollment_is_closed
|
37
|
+
@waiting_list.each{|student| puts "#{student}: waiting list is closed!"}
|
38
|
+
end
|
39
|
+
|
40
|
+
def notify_students_that_the_seminar_is_cancelled
|
41
|
+
(@students + @waiting_list).each{|student| puts "#{student}: the seminar has been cancelled!"}
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
include Golem
|
46
|
+
|
47
|
+
define_statemachine do
|
48
|
+
state :proposed do
|
49
|
+
on :schedule, :to => :scheduled
|
50
|
+
end
|
51
|
+
|
52
|
+
state :scheduled do
|
53
|
+
on :open, :to => :open_for_enrollment
|
54
|
+
end
|
55
|
+
|
56
|
+
state :open_for_enrollment do
|
57
|
+
on :close, :to => :closed_to_enrollment
|
58
|
+
on :enroll_student do
|
59
|
+
transition :if => :seats_available? do |seminar, student|
|
60
|
+
seminar.students << student
|
61
|
+
end
|
62
|
+
transition :to => :full, :if => Proc.new{|seminar, student| not seminar.student_is_enrolled?} do |seminar, student|
|
63
|
+
seminar.add_student_to_waiting_list(student)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
state :full do
|
69
|
+
on :move_to_bigger_classroom, :to => :open_for_enrollment
|
70
|
+
on :drop_student do
|
71
|
+
transition :to => :open_for_enrollment,
|
72
|
+
:if => Proc.new{|seminar, student| seminar.student_is_enrolled?(student) && seminar.waiting_list_is_empty?} do
|
73
|
+
seminar.students.delete student
|
74
|
+
end
|
75
|
+
transition :if => :student_is_enrolled? do |seminar, student|
|
76
|
+
seminar.students.delete student
|
77
|
+
seminar.enroll_student! seminar.waiting_list.shift
|
78
|
+
end
|
79
|
+
end
|
80
|
+
on :enroll_student do
|
81
|
+
transition :if => :seats_available? do |seminar, student|
|
82
|
+
seminar.students << student
|
83
|
+
end
|
84
|
+
transition :action => :add_student_to_waiting_list
|
85
|
+
end
|
86
|
+
on :close, :to => :closed_to_enrollment
|
87
|
+
enter :create_waiting_list
|
88
|
+
end
|
89
|
+
|
90
|
+
state :closed_to_enrollment do
|
91
|
+
enter :notify_waiting_list_that_enrollment_is_closed
|
92
|
+
end
|
93
|
+
|
94
|
+
state :cancelled do
|
95
|
+
enter :notify_students_that_the_seminar_is_cancelled
|
96
|
+
end
|
97
|
+
|
98
|
+
# The 'cancel' event can occur in all states.
|
99
|
+
all_states.each do |state|
|
100
|
+
state.on :cancel, :to => :cancelled
|
101
|
+
end
|
102
|
+
|
103
|
+
initial_state :proposed
|
104
|
+
current_state_from :status
|
105
|
+
|
106
|
+
on_all_transitions Proc.new{|obj, event, transition, event_args| puts "Transitioning from #{transition.from.name.inspect} to #{transition.to.name.inspect}"}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
s = Seminar.new
|
112
|
+
s.schedule!
|
113
|
+
s.open!
|
114
|
+
s.enroll_student! "bobby"
|
115
|
+
puts s.inspect
|
116
|
+
s.enroll_student! "eva"
|
117
|
+
puts s.inspect
|
118
|
+
s.enroll_student! "sally"
|
119
|
+
puts s.inspect
|
120
|
+
s.enroll_student! "matt"
|
121
|
+
puts s.inspect
|
122
|
+
s.enroll_student! "karina"
|
123
|
+
puts s.inspect
|
124
|
+
s.enroll_student! "tony"
|
125
|
+
puts s.inspect
|
126
|
+
s.enroll_student! "rich"
|
127
|
+
puts s.inspect
|
128
|
+
s.enroll_student! "suzie"
|
129
|
+
puts s.inspect
|
130
|
+
s.enroll_student! "fred"
|
131
|
+
puts s.inspect
|
132
|
+
s.drop_student! "sally"
|
133
|
+
puts s.inspect
|
134
|
+
s.drop_student! "bobby"
|
135
|
+
puts s.inspect
|
136
|
+
s.drop_student! "tony"
|
137
|
+
puts s.inspect
|
138
|
+
s.drop_student! "rich"
|
139
|
+
puts s.inspect
|
140
|
+
s.drop_student! "eva"
|
141
|
+
puts s.inspect
|