golem_statemachine 0.9
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/.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
|