stateology 0.1.3 → 0.1.6
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 +9 -0
- data/README.markdown +161 -0
- data/lib/stateology.rb +145 -86
- data/test/test.rb +239 -0
- metadata +5 -3
- data/README +0 -153
data/CHANGELOG
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
Stateology
|
2
|
+
==========
|
3
|
+
|
4
|
+
*Clean and fast Object state transitions in Ruby using the Mixology C extension.*
|
5
|
+
|
6
|
+
Supports:
|
7
|
+
* Dynamic switching between states (mixing and unmixing modules)
|
8
|
+
* Clean DSL-style syntax
|
9
|
+
* Optional state\_entry() and state\_exit() hooks for each state (automatically called upon state entry and exit)
|
10
|
+
* support for subclassing of classes that include Stateology (see below)
|
11
|
+
* support for nested states, i.e states defined within other states
|
12
|
+
|
13
|
+
Use as in the following:
|
14
|
+
|
15
|
+
class Sample
|
16
|
+
include Stateology
|
17
|
+
|
18
|
+
state(:Happy) {
|
19
|
+
def state_entry
|
20
|
+
puts "entering Happy state"
|
21
|
+
end
|
22
|
+
|
23
|
+
def do_something
|
24
|
+
puts "Pets a puppy"
|
25
|
+
end
|
26
|
+
|
27
|
+
def state_exit
|
28
|
+
puts "exiting Happy state"
|
29
|
+
end
|
30
|
+
}
|
31
|
+
|
32
|
+
state(:Angry) {
|
33
|
+
def state_entry
|
34
|
+
puts "entering Angry state"
|
35
|
+
end
|
36
|
+
|
37
|
+
def do_something
|
38
|
+
puts "Kicks a puppy"
|
39
|
+
end
|
40
|
+
|
41
|
+
def state_exit
|
42
|
+
puts "exiting Angry state"
|
43
|
+
end
|
44
|
+
}
|
45
|
+
|
46
|
+
# methods declared outside a 'state' are not part of any state
|
47
|
+
|
48
|
+
def state_entry
|
49
|
+
puts "entering Default state"
|
50
|
+
end
|
51
|
+
|
52
|
+
def do_something
|
53
|
+
puts "stares at the ceiling"
|
54
|
+
end
|
55
|
+
|
56
|
+
def state_exit
|
57
|
+
puts "exiting Default state"
|
58
|
+
end
|
59
|
+
|
60
|
+
# if we want the state_entry to run on instantiation
|
61
|
+
# we must call it from the initialize method
|
62
|
+
def initialize
|
63
|
+
state_entry
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
s = Sample.new
|
69
|
+
|
70
|
+
# in no state
|
71
|
+
s.do_something #=> "stares at the ceiling"
|
72
|
+
|
73
|
+
# now switch to Happy state
|
74
|
+
s.state :Happy
|
75
|
+
s.do_something #=> "Pets a puppy"
|
76
|
+
|
77
|
+
# now switch to Angry state
|
78
|
+
s.state :Angry
|
79
|
+
s.do_something #=> "Kicks a puppy"
|
80
|
+
|
81
|
+
# now switch back to no state
|
82
|
+
s.state nil
|
83
|
+
s.do_something #=> "stares at the ceiling"
|
84
|
+
|
85
|
+
UPDATE:
|
86
|
+
|
87
|
+
* made it so subclasses can inherit states from their superclasses e.g
|
88
|
+
|
89
|
+
|
90
|
+
class A
|
91
|
+
include Stateology
|
92
|
+
|
93
|
+
state(:Happy) {
|
94
|
+
def state_entry
|
95
|
+
puts "entering Happy state"
|
96
|
+
end
|
97
|
+
|
98
|
+
def hello
|
99
|
+
puts "hello from A"
|
100
|
+
end
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
class B < A
|
105
|
+
state(:Happy) {
|
106
|
+
def hello
|
107
|
+
puts "hello from B"
|
108
|
+
end
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
b = B.new
|
113
|
+
|
114
|
+
b.state :Happy
|
115
|
+
#=> "entering Happy state"
|
116
|
+
|
117
|
+
b.hello
|
118
|
+
#=> "hello from B"
|
119
|
+
|
120
|
+
* prior behaviour was for state\_entry not to exist in class B as Happy module from class A was overwritten by the new Happy module in B
|
121
|
+
* how does this fix work? the Happy module in B just includes any extant Happy module accessible in B
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
A FEW THINGS TO NOTE
|
127
|
+
--------------------
|
128
|
+
|
129
|
+
* When an object is instantiated it begins life in no state and only ordinary instance methods are accessible (The ordinary instance methods are those defined outside of any state() {} block)
|
130
|
+
|
131
|
+
* The ordinary instance methods are available to any state so long as they are not overridden by the state.
|
132
|
+
|
133
|
+
* To change from any given state to 'no state' pass nil as a parameter to the state method
|
134
|
+
e.g s.state nil
|
135
|
+
|
136
|
+
* 'no state', while not a state, may nonetheless have state\_entry() and state\_exit() methods; and these methods will be invoked on 'entry' and exit from 'no state'
|
137
|
+
|
138
|
+
* The state\_entry method for 'no state' is not automatically called on object instantiation. If you wish state\_entry to run when the object is instantiated invoke it in the initialize() method.
|
139
|
+
|
140
|
+
* The state\_entry method can also accept parameters:
|
141
|
+
e.g s.state :Happy, "hello"
|
142
|
+
In the above the string "hello" is passed as a parameter to the state\_entry() method of the Happy state.
|
143
|
+
|
144
|
+
* The #state method can accept either a Symbol (e.g :Happy) or a Module (e.g Happy or Sample::Happy). The following are equivalent:
|
145
|
+
s.state :Happy #=> change state to Happy
|
146
|
+
|
147
|
+
* The #state method can take a block; the block will be executed after the successful change of state:
|
148
|
+
e.g s.state(:Happy) { s.hello } #=> hello method invoked immediately after change of state as it's in the block
|
149
|
+
|
150
|
+
s.state Sample::Happy #=> equivalent to above (note the fully qualified name; as Happy is a module defined under the Sample class)
|
151
|
+
|
152
|
+
* alternatively; if the #state method is invoked internally by another instance method of the Sample class then a fully qualified module name is not required:
|
153
|
+
state Happy #=> Fully qualified module name not required when #state invoked in an instance method
|
154
|
+
|
155
|
+
* The #state method can also act as a 'getter' method when invoked with no parameters. It will return the current state name in Symbol form (e.g :Happy)
|
156
|
+
|
157
|
+
* The #state\_mod method works similarly to the #state 'getter' except it returns the Module representing the current state (e.g Sample::Happy)
|
158
|
+
|
159
|
+
* The #state?(state\_name) returns boolean true if the current state is equal to state\_name, and false if not. state\_name can be either a Module or a Symbol
|
160
|
+
|
161
|
+
|
data/lib/stateology.rb
CHANGED
@@ -7,7 +7,8 @@ end
|
|
7
7
|
require 'mixology'
|
8
8
|
|
9
9
|
module Stateology
|
10
|
-
|
10
|
+
VERSION = "0.1.6"
|
11
|
+
|
11
12
|
# alternative to 'nil'
|
12
13
|
Default = nil
|
13
14
|
|
@@ -18,113 +19,171 @@ module Stateology
|
|
18
19
|
|
19
20
|
# class methods
|
20
21
|
module SM_Class_Methods
|
21
|
-
def state(name, &block)
|
22
|
-
|
23
|
-
|
24
|
-
if constants.include?(name.to_s) then
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
m
|
22
|
+
def state(name, &block)
|
23
|
+
|
24
|
+
# if const is defined here then module_eval
|
25
|
+
if constants.include?(name.to_s) && const_defined?(name) then
|
26
|
+
self.module_eval(&block)
|
27
|
+
else
|
28
|
+
|
29
|
+
m = Module.new
|
30
|
+
# if the state is defined further up the chain then "inherit it"
|
31
|
+
if constants.include?(name.to_s) && !const_defined?(name) then
|
32
|
+
# if constant not defined here then must be inherited
|
33
|
+
inherited_state = const_get(name)
|
34
|
+
|
35
|
+
# ignore if the constant is not a module
|
36
|
+
m.send(:include, inherited_state) if inherited_state.instance_of?(Module)
|
37
|
+
end
|
38
|
+
|
39
|
+
m.send(:include, Stateology)
|
40
|
+
m.module_eval(&block)
|
41
|
+
|
42
|
+
const_set(name, m)
|
29
43
|
end
|
30
|
-
|
31
|
-
const_set(name, m)
|
32
|
-
|
33
44
|
end
|
34
45
|
end
|
35
|
-
|
46
|
+
|
47
|
+
# strip the class path and return just the constant name, i.e Hello::Fren -> Fren
|
48
|
+
def __elided_class_path(sym)
|
49
|
+
"#{sym}".split(/::/).last.intern
|
50
|
+
end
|
51
|
+
|
52
|
+
def __sym_to_mod(sym)
|
53
|
+
class << self; self; end.const_get(sym)
|
54
|
+
end
|
55
|
+
|
56
|
+
def __mod_to_sym(mod)
|
57
|
+
# weird case where module does not have name (i.e when a state created on the eigenclass)
|
58
|
+
if mod.name == "" then
|
59
|
+
class << self; self; end.constants.each do |v|
|
60
|
+
return v.to_sym if __sym_to_mod(v.to_sym) == mod
|
61
|
+
end
|
62
|
+
return :ConstantNotDefined
|
63
|
+
end
|
64
|
+
|
65
|
+
mod.name.to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
# is state_name a nested state?
|
69
|
+
def __nested_state?(new_state)
|
70
|
+
|
71
|
+
# test is:
|
72
|
+
# (1) are we currently in a state? (non nil)
|
73
|
+
# (2) is the new state a state? (non nil)
|
74
|
+
# (3) is the new state defined under the current state? (i.e it's nested)
|
75
|
+
__current_state &&
|
76
|
+
new_state &&
|
77
|
+
__current_state.const_defined?(__elided_class_path(__mod_to_sym(new_state)))
|
78
|
+
end
|
79
|
+
|
36
80
|
# instance methods
|
37
|
-
|
38
|
-
|
39
|
-
|
81
|
+
def __state_epilogue
|
82
|
+
|
83
|
+
@__SM_nesting.each do |old_state|
|
84
|
+
raise NameError if !old_state.instance_of?(Module) && old_state != nil
|
85
|
+
|
86
|
+
begin
|
87
|
+
state_exit()
|
88
|
+
rescue NoMethodError
|
89
|
+
# do nothing
|
90
|
+
end
|
91
|
+
|
92
|
+
if old_state then unmix(old_state) end
|
93
|
+
end
|
94
|
+
@__SM_nesting = []
|
95
|
+
end
|
96
|
+
|
97
|
+
def __state_prologue(new_state, state_args, &block)
|
98
|
+
|
40
99
|
# ensure that the constant is a module
|
41
|
-
raise NameError if
|
42
|
-
|
100
|
+
raise NameError if !new_state.instance_of?(Module) && new_state != nil
|
101
|
+
|
102
|
+
# only mixin if non-nil (duh)
|
103
|
+
if new_state then extend(new_state) end
|
104
|
+
|
43
105
|
begin
|
44
|
-
|
106
|
+
state_entry(*state_args, &block)
|
45
107
|
rescue NoMethodError
|
46
108
|
# do nothing
|
47
109
|
end
|
48
|
-
|
49
|
-
if old_state then unmix(old_state) end
|
110
|
+
|
50
111
|
end
|
51
|
-
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
# only mixin if
|
58
|
-
if new_state then mixin(new_state) end
|
59
|
-
|
60
|
-
begin
|
61
|
-
state_entry(*state_args)
|
62
|
-
rescue NoMethodError
|
63
|
-
# do nothing
|
112
|
+
|
113
|
+
def __validate_state_name(state_name)
|
114
|
+
# if we receive a Symbol convert it to a constant
|
115
|
+
if Symbol === state_name then
|
116
|
+
state_name = __sym_to_mod(state_name)
|
64
117
|
end
|
65
|
-
|
118
|
+
|
119
|
+
raise NameError if state_name && !state_name.instance_of?(Module)
|
120
|
+
|
121
|
+
state_name
|
122
|
+
end
|
123
|
+
|
124
|
+
def __state_transition(new_state, state_args, &block)
|
125
|
+
# preven unnecessary state transition
|
126
|
+
return if __current_state == new_state
|
127
|
+
|
128
|
+
# get rid of state_name from arg list
|
129
|
+
state_args.shift
|
130
|
+
|
131
|
+
# exit old state only if the new state is not nested within it
|
132
|
+
__state_epilogue unless __nested_state?(new_state)
|
133
|
+
__state_prologue(new_state, state_args, &block)
|
134
|
+
|
135
|
+
@__SM_nesting.unshift(new_state)
|
136
|
+
end
|
137
|
+
|
138
|
+
def __state_getter
|
139
|
+
__current_state ? __elided_class_path(__mod_to_sym(__current_state)) : nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def __current_state
|
143
|
+
@__SM_nesting ||= [nil]
|
144
|
+
@__SM_nesting.first
|
66
145
|
end
|
67
|
-
|
146
|
+
|
68
147
|
def state(*state_args, &block)
|
69
|
-
|
148
|
+
|
70
149
|
# behave as getter
|
71
|
-
if
|
72
|
-
return
|
73
|
-
end
|
74
|
-
|
75
|
-
# behave as setter (only care about first argument)
|
76
|
-
state_name = state_args.shift
|
77
|
-
|
78
|
-
# if we receive a Symbol convert it to a constant
|
79
|
-
if(Symbol === state_name) then
|
80
|
-
state_name = self.class.const_get(state_name)
|
150
|
+
if state_args.empty? then
|
151
|
+
return __state_getter
|
81
152
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# update the current state variable
|
94
|
-
@__SM_cur_state = state_name
|
95
|
-
|
96
|
-
# if we were given a block, run it now
|
97
|
-
if(block) then yield end
|
98
|
-
|
99
|
-
rescue NameError
|
100
|
-
raise NameError, "#{state_name} not a valid state"
|
101
|
-
|
153
|
+
|
154
|
+
new_state = __validate_state_name(state_args.first)
|
155
|
+
|
156
|
+
__state_transition(new_state, state_args, &block)
|
157
|
+
|
158
|
+
# return value is the current state
|
159
|
+
__current_state
|
160
|
+
|
161
|
+
rescue NameError
|
162
|
+
raise NameError, "#{new_state} not a valid state"
|
163
|
+
|
102
164
|
end
|
103
|
-
|
165
|
+
|
104
166
|
# is the current state equal to state_name?
|
105
167
|
def state?(state_name)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
rescue NameError
|
117
|
-
raise NameError, "#{state_name} not a valid state"
|
118
|
-
|
168
|
+
|
169
|
+
state_name = __validate_state_name(state_name)
|
170
|
+
|
171
|
+
# compare
|
172
|
+
state_name == __current_state
|
173
|
+
|
174
|
+
rescue NameError
|
175
|
+
raise NameError, "#{state_name} not a valid state"
|
176
|
+
|
119
177
|
end
|
120
|
-
|
178
|
+
|
121
179
|
# return the current state as a module
|
122
180
|
def state_mod
|
123
|
-
|
181
|
+
__current_state
|
124
182
|
end
|
125
|
-
|
126
|
-
private :__state_prologue, :__state_epilogue
|
127
|
-
|
183
|
+
|
184
|
+
private :__state_prologue, :__state_epilogue, :__elided_class_path, :__mod_to_sym, :__sym_to_mod,
|
185
|
+
:__nested_state?, :__current_state, :__validate_state_name, :__state_transition, :__state_getter
|
186
|
+
|
128
187
|
end
|
129
188
|
|
130
189
|
|
data/test/test.rb
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require '../lib/stateology'
|
3
|
+
|
4
|
+
class Object
|
5
|
+
def meta
|
6
|
+
class << self; self; end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ParentState
|
11
|
+
include Stateology
|
12
|
+
attr_reader :state_entry_check
|
13
|
+
attr_reader :state_exit_check
|
14
|
+
|
15
|
+
def state_entry
|
16
|
+
@state_entry_check = "entry_nil"
|
17
|
+
end
|
18
|
+
|
19
|
+
def state_exit
|
20
|
+
@state_exit_check = "exit_nil"
|
21
|
+
end
|
22
|
+
|
23
|
+
state(:State1) {
|
24
|
+
|
25
|
+
def state_entry
|
26
|
+
@state_entry_check = "entry_state1"
|
27
|
+
end
|
28
|
+
|
29
|
+
def act
|
30
|
+
1
|
31
|
+
end
|
32
|
+
|
33
|
+
def state1_act
|
34
|
+
1
|
35
|
+
end
|
36
|
+
|
37
|
+
def state_exit
|
38
|
+
@state_exit_check = "exit_state1"
|
39
|
+
end
|
40
|
+
|
41
|
+
state(:State1_nested) {
|
42
|
+
def state_entry(&block)
|
43
|
+
puts "balls-deep in State1_nested!"
|
44
|
+
if block then yield end
|
45
|
+
end
|
46
|
+
def act
|
47
|
+
1.5
|
48
|
+
end
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
state(:State2) {
|
53
|
+
def act
|
54
|
+
2
|
55
|
+
end
|
56
|
+
|
57
|
+
def state2_act
|
58
|
+
2
|
59
|
+
end
|
60
|
+
}
|
61
|
+
|
62
|
+
|
63
|
+
def act
|
64
|
+
0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class ChildState < ParentState
|
69
|
+
state(:State1) {
|
70
|
+
def act_child
|
71
|
+
1
|
72
|
+
end
|
73
|
+
}
|
74
|
+
|
75
|
+
state(:State2) {
|
76
|
+
def act_child
|
77
|
+
2
|
78
|
+
end
|
79
|
+
}
|
80
|
+
|
81
|
+
def act
|
82
|
+
0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class StateologyTest < Test::Unit::TestCase
|
87
|
+
puts "testing Stateology #{Stateology::VERSION}"
|
88
|
+
|
89
|
+
def test_nil_state
|
90
|
+
s = ParentState.new
|
91
|
+
assert_equal(0, s.act)
|
92
|
+
assert_equal(nil, s.state_exit_check)
|
93
|
+
assert_equal(nil, s.state_entry_check)
|
94
|
+
assert_raises(NoMethodError) { s.state1_act }
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_transition_from_nil_state
|
98
|
+
s = ParentState.new
|
99
|
+
assert_equal(0, s.act)
|
100
|
+
assert_equal(nil, s.state_exit_check)
|
101
|
+
assert_equal(nil, s.state_entry_check)
|
102
|
+
s.state :State1
|
103
|
+
assert_equal("exit_nil", s.state_exit_check)
|
104
|
+
assert_equal("entry_state1", s.state_entry_check)
|
105
|
+
assert_equal(1, s.act)
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_transition_to_nil_state
|
109
|
+
s = ParentState.new
|
110
|
+
s.state :State1
|
111
|
+
assert_equal(1, s.act)
|
112
|
+
s.state nil
|
113
|
+
assert_equal("exit_state1", s.state_exit_check)
|
114
|
+
assert_equal("entry_nil", s.state_entry_check)
|
115
|
+
assert_equal(0, s.act)
|
116
|
+
assert_raises(NoMethodError) { s.state1_act }
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_transition_from_state1_to_state2
|
120
|
+
s = ParentState.new
|
121
|
+
s.state :State1
|
122
|
+
assert_equal(1, s.act)
|
123
|
+
assert_raises(NoMethodError) { s.state2_act }
|
124
|
+
s.state :State2
|
125
|
+
assert_equal(2, s.act)
|
126
|
+
assert_raises(NoMethodError) { s.state1_act }
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_inheritance_of_state
|
130
|
+
s = ChildState.new
|
131
|
+
s.state :State1
|
132
|
+
|
133
|
+
# testing inherited state methods
|
134
|
+
assert_equal(1, s.act)
|
135
|
+
assert_equal(1, s.state1_act)
|
136
|
+
|
137
|
+
# testing own method
|
138
|
+
assert_equal(1, s.act_child)
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_cant_transition_to_nested_from_nil
|
142
|
+
s = ParentState.new
|
143
|
+
assert_raises(NameError){ s.state(:State1_nested1) }
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
def test_nested_state
|
148
|
+
s = ParentState.new
|
149
|
+
s.state :State1
|
150
|
+
s.state :State1_nested
|
151
|
+
assert_equal(1.5, s.act)
|
152
|
+
assert_equal(1, s.state1_act)
|
153
|
+
s.state nil
|
154
|
+
assert_raises(NoMethodError) { s.state1_act }
|
155
|
+
assert_equal(0, s.act)
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_state_getter
|
159
|
+
s = ParentState.new
|
160
|
+
assert_equal(nil, s.state)
|
161
|
+
|
162
|
+
s.state :State1
|
163
|
+
assert_equal(:State1, s.state)
|
164
|
+
|
165
|
+
s.state :State1_nested
|
166
|
+
assert_equal(:State1_nested, s.state)
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_state_compare
|
170
|
+
s = ParentState.new
|
171
|
+
assert_equal(true, s.state?(nil))
|
172
|
+
|
173
|
+
s.state :State1
|
174
|
+
assert_equal(false, s.state?(nil))
|
175
|
+
assert_equal(true, s.state?(:State1))
|
176
|
+
|
177
|
+
s.state :State1_nested
|
178
|
+
assert_equal(false, s.state?(:State1))
|
179
|
+
assert_equal(true, s.state?(:State1_nested))
|
180
|
+
|
181
|
+
s.state nil
|
182
|
+
assert_equal(true, s.state?(nil))
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_state_defined_on_singleton
|
186
|
+
s = ParentState.new
|
187
|
+
|
188
|
+
class << s
|
189
|
+
state(:Sing_state) {
|
190
|
+
def state_entry
|
191
|
+
@state_entry_check = "sing_entry"
|
192
|
+
end
|
193
|
+
|
194
|
+
def act
|
195
|
+
99
|
196
|
+
end
|
197
|
+
|
198
|
+
def state_exit
|
199
|
+
@state_exit_check = "sing_exit"
|
200
|
+
end
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
204
|
+
assert_equal(0, s.act)
|
205
|
+
|
206
|
+
s.state :Sing_state
|
207
|
+
|
208
|
+
# test the getter
|
209
|
+
assert_equal(:Sing_state, s.state)
|
210
|
+
|
211
|
+
# test state_entry
|
212
|
+
assert_equal("sing_entry", s.state_entry_check)
|
213
|
+
|
214
|
+
# test the act function
|
215
|
+
assert_equal(99, s.act)
|
216
|
+
|
217
|
+
# test state compare
|
218
|
+
assert_equal(true, s.state?(:Sing_state))
|
219
|
+
|
220
|
+
s.state nil
|
221
|
+
|
222
|
+
# test state_exit
|
223
|
+
assert_equal("sing_exit", s.state_exit_check)
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
|
231
|
+
|
232
|
+
|
233
|
+
|
234
|
+
|
235
|
+
|
236
|
+
|
237
|
+
|
238
|
+
|
239
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stateology
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Mair
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-12-03 00:00:00 +13:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -31,10 +31,12 @@ extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
32
32
|
|
33
33
|
files:
|
34
|
-
- README
|
34
|
+
- README.markdown
|
35
35
|
- LICENSE
|
36
36
|
- lib/stateology.rb
|
37
37
|
- sample.rb
|
38
|
+
- CHANGELOG
|
39
|
+
- test/test.rb
|
38
40
|
has_rdoc: false
|
39
41
|
homepage: http://banisterfiend.wordpress.com
|
40
42
|
post_install_message:
|
data/README
DELETED
@@ -1,153 +0,0 @@
|
|
1
|
-
Clean and fast Object state transitions in Ruby using the Mixology C extension.
|
2
|
-
|
3
|
-
Supports:
|
4
|
-
* Dynamic switching between states (mixing and unmixing modules)
|
5
|
-
* Clean DSL-style syntax
|
6
|
-
* Optional state_entry() and state_exit() hooks for each state (automatically called upon state entry and exit)
|
7
|
-
* support for subclassing of classes that include Stateology (see below)
|
8
|
-
|
9
|
-
Use as in the following:
|
10
|
-
|
11
|
-
class Sample
|
12
|
-
include Stateology
|
13
|
-
|
14
|
-
state(:Happy) {
|
15
|
-
def state_entry
|
16
|
-
puts "entering Happy state"
|
17
|
-
end
|
18
|
-
|
19
|
-
def do_something
|
20
|
-
puts "Pets a puppy"
|
21
|
-
end
|
22
|
-
|
23
|
-
def state_exit
|
24
|
-
puts "exiting Happy state"
|
25
|
-
end
|
26
|
-
}
|
27
|
-
|
28
|
-
state(:Angry) {
|
29
|
-
def state_entry
|
30
|
-
puts "entering Angry state"
|
31
|
-
end
|
32
|
-
|
33
|
-
def do_something
|
34
|
-
puts "Kicks a puppy"
|
35
|
-
end
|
36
|
-
|
37
|
-
def state_exit
|
38
|
-
puts "exiting Angry state"
|
39
|
-
end
|
40
|
-
}
|
41
|
-
|
42
|
-
# methods declared outside a 'state' are not part of any state
|
43
|
-
|
44
|
-
def state_entry
|
45
|
-
puts "entering Default state"
|
46
|
-
end
|
47
|
-
|
48
|
-
def do_something
|
49
|
-
puts "stares at the ceiling"
|
50
|
-
end
|
51
|
-
|
52
|
-
def state_exit
|
53
|
-
puts "exiting Default state"
|
54
|
-
end
|
55
|
-
|
56
|
-
# if we want the state_entry to run on instantiation
|
57
|
-
# we must call it from the initialize method
|
58
|
-
def initialize
|
59
|
-
state_entry
|
60
|
-
end
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
s = Sample.new
|
65
|
-
|
66
|
-
# in no state
|
67
|
-
s.do_something #=> "stares at the ceiling"
|
68
|
-
|
69
|
-
# now switch to Happy state
|
70
|
-
s.state :Happy
|
71
|
-
s.do_something #=> "Pets a puppy"
|
72
|
-
|
73
|
-
# now switch to Angry state
|
74
|
-
s.state :Angry
|
75
|
-
s.do_something #=> "Kicks a puppy"
|
76
|
-
|
77
|
-
# now switch back to no state
|
78
|
-
s.state nil
|
79
|
-
s.do_something #=> "stares at the ceiling"
|
80
|
-
|
81
|
-
UPDATE:
|
82
|
-
* made it so subclasses can inherit states from their superclasses e.g
|
83
|
-
class A
|
84
|
-
include Stateology
|
85
|
-
|
86
|
-
state(:Happy) {
|
87
|
-
def state_entry
|
88
|
-
puts "entering Happy state"
|
89
|
-
end
|
90
|
-
|
91
|
-
def hello
|
92
|
-
puts "hello from A"
|
93
|
-
end
|
94
|
-
}
|
95
|
-
end
|
96
|
-
|
97
|
-
class B < A
|
98
|
-
state(:Happy) {
|
99
|
-
def hello
|
100
|
-
puts "hello from B"
|
101
|
-
end
|
102
|
-
}
|
103
|
-
end
|
104
|
-
|
105
|
-
b = B.new
|
106
|
-
|
107
|
-
b.state :Happy
|
108
|
-
#=> "entering Happy state"
|
109
|
-
|
110
|
-
b.hello
|
111
|
-
#=> "hello from B"
|
112
|
-
|
113
|
-
* prior behaviour was for state_entry not to exist in class B as Happy module from class A was overwritten by the new Happy module in B
|
114
|
-
* how does this fix work? the Happy module in B just includes any extant Happy module accessible in B
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
---=A FEW THINGS TO NOTE=---
|
120
|
-
|
121
|
-
* When an object is instantiated it begins life in no state and only ordinary instance methods are accessible (The ordinary instance methods are those defined outside of any state() {} block)
|
122
|
-
|
123
|
-
* The ordinary instance methods are available to any state so long as they are not overridden by the state.
|
124
|
-
|
125
|
-
* To change from any given state to 'no state' pass nil as a parameter to the state method
|
126
|
-
e.g s.state nil
|
127
|
-
|
128
|
-
* 'no state', while not a state, may nonetheless have state_entry() and state_exit() methods; and these methods will be invoked on 'entry' and exit from 'no state'
|
129
|
-
|
130
|
-
* The state_entry method for 'no state' is not automatically called on object instantiation. If you wish state_entry to run when the object is instantiated invoke it in the initialize() method.
|
131
|
-
|
132
|
-
* The state_entry method can also accept parameters:
|
133
|
-
e.g s.state :Happy, "hello"
|
134
|
-
In the above the string "hello" is passed as a parameter to the state_entry() method of the Happy state.
|
135
|
-
|
136
|
-
* The #state method can accept either a Symbol (e.g :Happy) or a Module (e.g Happy or Sample::Happy). The following are equivalent:
|
137
|
-
s.state :Happy #=> change state to Happy
|
138
|
-
|
139
|
-
* The #state method can take a block; the block will be executed after the successful change of state:
|
140
|
-
e.g s.state(:Happy) { s.hello } #=> hello method invoked immediately after change of state as it's in the block
|
141
|
-
|
142
|
-
s.state Sample::Happy #=> equivalent to above (note the fully qualified name; as Happy is a module defined under the Sample class)
|
143
|
-
|
144
|
-
* alternatively; if the #state method is invoked internally by another instance method of the Sample class then a fully qualified module name is not required:
|
145
|
-
state Happy #=> Fully qualified module name not required when #state invoked in an instance method
|
146
|
-
|
147
|
-
* The #state method can also act as a 'getter' method when invoked with no parameters. It will return the current state name in Symbol form (e.g :Happy)
|
148
|
-
|
149
|
-
* The #state_mod method works similarly to the #state 'getter' except it returns the Module representing the current state (e.g Sample::Happy)
|
150
|
-
|
151
|
-
* The #state?(state_name) returns boolean true if the current state is equal to state_name, and false if not. state_name can be either a Module or a Symbol
|
152
|
-
|
153
|
-
* One last note: state(:Name) {} is just DSL-style syntactic sugar for module Name...end
|