yasm 0.0.1
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/README.markdown +228 -0
- data/VERSION +1 -0
- data/lib/yasm.rb +10 -0
- data/lib/yasm/action.rb +29 -0
- data/lib/yasm/context.rb +58 -0
- data/lib/yasm/context/anonymous_state_identifier.rb +5 -0
- data/lib/yasm/context/state_configuration.rb +15 -0
- data/lib/yasm/context/state_container.rb +31 -0
- data/lib/yasm/conversions.rb +2 -0
- data/lib/yasm/conversions/class.rb +17 -0
- data/lib/yasm/conversions/symbol.rb +13 -0
- data/lib/yasm/manager.rb +36 -0
- data/lib/yasm/state.rb +34 -0
- data/lib/yasm/version.rb +3 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/yasm/context_spec.rb +65 -0
- data/spec/yasm/conversions_spec.rb +43 -0
- data/spec/yasm/manager_spec.rb +89 -0
- data/spec/yasm/state_container_spec.rb +25 -0
- data/spec/yasm/state_spec.rb +33 -0
- data/yasm.gemspec +62 -0
- metadata +122 -0
data/README.markdown
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
# YASM - Yet Another State Machine
|
2
|
+
|
3
|
+
Pronounced "yaz-um."
|
4
|
+
|
5
|
+
|
6
|
+
## Install?
|
7
|
+
|
8
|
+
$ gem install yasm
|
9
|
+
|
10
|
+
## Why?
|
11
|
+
|
12
|
+
In a state machine, there are states, contexts, and actions. Actions have side-effects, conditional logic, etc. States have various allowable actions.
|
13
|
+
Contexts can support various states. All beg to be defined in classes. The other ruby state machines out there are great. But they all have hashitis.
|
14
|
+
Classes and mixins are the cure.
|
15
|
+
|
16
|
+
## How?
|
17
|
+
|
18
|
+
Let's create a state machine for a vending machine. What does a vending machine do? It lets you input money and make a selection. When you make a selection,
|
19
|
+
it vends the selection. Let's start off with a really simple model of this:
|
20
|
+
|
21
|
+
class VendingMachine
|
22
|
+
include Yasm::Context
|
23
|
+
|
24
|
+
start :waiting
|
25
|
+
end
|
26
|
+
|
27
|
+
class Waiting; include Yasm::State; end
|
28
|
+
|
29
|
+
class Vending; include Yasm::State; end
|
30
|
+
|
31
|
+
So far, we've created a context (a thing that has state), given it a start state, and then defined a couple of states (Waiting, Vending).
|
32
|
+
|
33
|
+
So, how do we use this vending machine? We'll need to create some actions first:
|
34
|
+
|
35
|
+
class InputMoney
|
36
|
+
include Yasm::Action
|
37
|
+
end
|
38
|
+
|
39
|
+
class MakeSelection
|
40
|
+
include Yasm::Action
|
41
|
+
|
42
|
+
triggers :vending
|
43
|
+
end
|
44
|
+
|
45
|
+
class RetrieveSelection
|
46
|
+
include Yasm::Action
|
47
|
+
|
48
|
+
triggers :waiting
|
49
|
+
end
|
50
|
+
|
51
|
+
And now we can run a simulation:
|
52
|
+
|
53
|
+
vending_machine = VendingMachine.new
|
54
|
+
|
55
|
+
vending_machine.state.value
|
56
|
+
#==> Waiting
|
57
|
+
|
58
|
+
vending_machine.do! InputMoney
|
59
|
+
|
60
|
+
vending_machine.state.value
|
61
|
+
#==> Waiting
|
62
|
+
|
63
|
+
vending_machine.do! MakeSelection
|
64
|
+
|
65
|
+
vending_machine.state.value
|
66
|
+
#==> Vending
|
67
|
+
|
68
|
+
vending_machine.do! RetrieveSelection
|
69
|
+
|
70
|
+
vending_machine.state.value
|
71
|
+
#==> Waiting
|
72
|
+
|
73
|
+
There's some problems, though. Our simple state machine is a little too simple; someone could make a selection without inputing any money.
|
74
|
+
We need a way to limit the actions that can be applied to our vending machine based on it's current state. How do we do that? Let's redefine
|
75
|
+
our states, using the actions macro:
|
76
|
+
|
77
|
+
class Waiting
|
78
|
+
include Yasm::State
|
79
|
+
|
80
|
+
actions :input_money, :make_selection
|
81
|
+
end
|
82
|
+
|
83
|
+
class Vending
|
84
|
+
include Yasm::State
|
85
|
+
|
86
|
+
actions :retrieve_selection
|
87
|
+
end
|
88
|
+
|
89
|
+
Now, when the vending machine is in the `Waiting` state, the only actions we can apply to it are `InputMoney` and `MakeSelection`. If we try to apply
|
90
|
+
invalid actions to the context, `Yasm` will raise an exception.
|
91
|
+
|
92
|
+
vending_machine.state.value
|
93
|
+
#==> Waiting
|
94
|
+
|
95
|
+
vending_machine.do! RetrieveSelection
|
96
|
+
#==> RuntimeError: We're sorry, but the action `RetrieveSelection`
|
97
|
+
is not possible given the current state `Waiting`.
|
98
|
+
|
99
|
+
vending_machine.do! InputMoney
|
100
|
+
|
101
|
+
vending_machine.state.value
|
102
|
+
#==> Waiting
|
103
|
+
|
104
|
+
## Side Effects
|
105
|
+
|
106
|
+
How can we take our simulation farther? A real vending machine would verify that when you make a selection,
|
107
|
+
you actually have input enough money to pay for that selection. How can we model this?
|
108
|
+
|
109
|
+
For starters, we'll need to add a property to our `VendingMachine`
|
110
|
+
that lets us keep track of how much money was input. We'll also need to initialize our `InputMoney` actions with an amount.
|
111
|
+
|
112
|
+
class VendingMachine
|
113
|
+
include Yasm::Context
|
114
|
+
start :waiting
|
115
|
+
|
116
|
+
attr_accessor :amount_input
|
117
|
+
|
118
|
+
def initialize
|
119
|
+
@amount_input = 0
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class InputMoney
|
124
|
+
include Yasm::Action
|
125
|
+
|
126
|
+
def initialize(amount_input)
|
127
|
+
@amount_input = amount_input
|
128
|
+
end
|
129
|
+
|
130
|
+
def execute
|
131
|
+
context.amount_input += @amount_input
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
Notice I defined the `execute` method on the action. This is the method that gets run whenever an action gets applied to a state container
|
136
|
+
(e.g., `vending_machine.do! InputMoney`). This is where you create side effects.
|
137
|
+
|
138
|
+
Now we can try out adding money into our vending machine:
|
139
|
+
|
140
|
+
vending_machine.amount_input
|
141
|
+
# ==> 0
|
142
|
+
|
143
|
+
vending_machine.do! InputMoney.new(10)
|
144
|
+
|
145
|
+
vending_machine.amount_input
|
146
|
+
# ==> 10
|
147
|
+
|
148
|
+
As for verifying that we have input enough money to pay for the selection we've chosen, we'll need to create an item, then add that to our `MakeSelection` class:
|
149
|
+
|
150
|
+
class SnickersBar
|
151
|
+
def self.price; 30; end
|
152
|
+
end
|
153
|
+
|
154
|
+
class MakeSelection
|
155
|
+
include Yasm::Action
|
156
|
+
|
157
|
+
def initialize(selection)
|
158
|
+
@selection = selection
|
159
|
+
end
|
160
|
+
|
161
|
+
def execute
|
162
|
+
if context.amount_input >= @selection.price
|
163
|
+
trigger Vending
|
164
|
+
else
|
165
|
+
raise "We're sorry, but you have not input enough money for a #{@selection}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
Notice that we called the `trigger` method inside the `execute` method instead of calling the `triggers` macro on the action. This way,
|
171
|
+
we can conditionally move to the next logical state only when our conditions have been met (in this case, that we've input enough money to
|
172
|
+
pay for our selection).
|
173
|
+
|
174
|
+
v = VendingMachine.new
|
175
|
+
|
176
|
+
v.amount_input
|
177
|
+
#==> 0
|
178
|
+
|
179
|
+
v.do! MakeSelection.new(SnickersBar)
|
180
|
+
#==> RuntimeError: We're sorry, but you have not input enough money for a SnickersBar
|
181
|
+
|
182
|
+
v.do! InputMoney.new(10)
|
183
|
+
|
184
|
+
v.do! MakeSelection.new(SnickersBar)
|
185
|
+
#==> RuntimeError: We're sorry, but you have not input enough money for a SnickersBar
|
186
|
+
|
187
|
+
v.do! InputMoney.new(20)
|
188
|
+
|
189
|
+
v.do! MakeSelection.new(SnickersBar)
|
190
|
+
|
191
|
+
v.state.value
|
192
|
+
#==> Vending
|
193
|
+
|
194
|
+
v.do! RetrieveSelection
|
195
|
+
|
196
|
+
v.state.value
|
197
|
+
#==> Waiting
|
198
|
+
|
199
|
+
|
200
|
+
## End states
|
201
|
+
|
202
|
+
Sometimes, a state is final. Like, what if, out of frustration, you threw the vending machine off the top of a 10 story building? It's probably not going
|
203
|
+
to work again after that. You can use the `final!` macro on a state to denote that this is the end.
|
204
|
+
|
205
|
+
class TossOffBuilding
|
206
|
+
include Yasm::Action
|
207
|
+
|
208
|
+
triggers :obliterated
|
209
|
+
end
|
210
|
+
|
211
|
+
class Obliterated
|
212
|
+
include Yasm::State
|
213
|
+
|
214
|
+
final!
|
215
|
+
end
|
216
|
+
|
217
|
+
vending_machine = VendingMachine.new
|
218
|
+
|
219
|
+
vending_machine.do! TossOffBuilding
|
220
|
+
|
221
|
+
vending_machine.do! MakeSelection.new(SnickersBar)
|
222
|
+
#==> RuntimeError: We're sorry, but the current state `Obliterated` is final. It does not accept any actions
|
223
|
+
|
224
|
+
|
225
|
+
|
226
|
+
## PUBLIC DOMAIN
|
227
|
+
|
228
|
+
This software is committed to the public domain. No license. No copyright. DO ANYTHING!
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/yasm.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'active_support/core_ext/string'
|
2
|
+
require 'yasm/conversions'
|
3
|
+
require 'yasm/manager'
|
4
|
+
require 'yasm/version'
|
5
|
+
require 'yasm/state'
|
6
|
+
require 'yasm/action'
|
7
|
+
require 'yasm/context/anonymous_state_identifier'
|
8
|
+
require 'yasm/context/state_configuration'
|
9
|
+
require 'yasm/context/state_container'
|
10
|
+
require 'yasm/context'
|
data/lib/yasm/action.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Yasm
|
2
|
+
module Action
|
3
|
+
attr_accessor :context, :state_container
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def triggers(state)
|
11
|
+
@trigger_state = state
|
12
|
+
end
|
13
|
+
|
14
|
+
def trigger_state; @trigger_state; end
|
15
|
+
end
|
16
|
+
|
17
|
+
def triggers
|
18
|
+
self.class.trigger_state
|
19
|
+
end
|
20
|
+
|
21
|
+
def trigger(state)
|
22
|
+
Yasm::Manager.change_state :to => state, :on => state_container
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/yasm/context.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Yasm
|
2
|
+
module Context
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# for a simple, anonymous state
|
9
|
+
def start(state)
|
10
|
+
state_configurations[ANONYMOUS_STATE] = StateConfiguration.new
|
11
|
+
state_configurations[ANONYMOUS_STATE].start state
|
12
|
+
end
|
13
|
+
|
14
|
+
# for a named state
|
15
|
+
def state(name, &block)
|
16
|
+
raise ArgumentError, "The state name must respond to `to_sym`" unless name.respond_to?(:to_sym)
|
17
|
+
name = name.to_sym
|
18
|
+
state_configurations[name] = StateConfiguration.new
|
19
|
+
state_configurations[name].instance_eval &block
|
20
|
+
|
21
|
+
raise "You must provide a start state for #{name}" unless state_configurations[name].start_state
|
22
|
+
|
23
|
+
define_method(name) { state_container name }
|
24
|
+
end
|
25
|
+
|
26
|
+
# state configuration metadata
|
27
|
+
def state_configurations
|
28
|
+
@state_configurations ||= {}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def do!(*actions)
|
33
|
+
state.do! *actions
|
34
|
+
end
|
35
|
+
|
36
|
+
def state
|
37
|
+
raise "This class has no anonymous state" unless self.class.state_configurations[ANONYMOUS_STATE]
|
38
|
+
state_container ANONYMOUS_STATE
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def state_containers
|
43
|
+
@state_containers ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def state_container(id)
|
47
|
+
unless state_containers[id]
|
48
|
+
state_containers[id] =
|
49
|
+
StateContainer.new(
|
50
|
+
:context => self,
|
51
|
+
:state => self.class.state_configurations[id].start_state.to_class.new
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
state_containers[id]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Yasm
|
2
|
+
module Context
|
3
|
+
class StateContainer
|
4
|
+
attr_accessor :context
|
5
|
+
attr_accessor :state #:nodoc:
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@context = options[:context]
|
9
|
+
@state = options[:state]
|
10
|
+
end
|
11
|
+
|
12
|
+
def value; @state; end
|
13
|
+
|
14
|
+
def state=(s)
|
15
|
+
if s.class == Class
|
16
|
+
@state = s.new
|
17
|
+
else
|
18
|
+
@state = s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def do!(*actions)
|
23
|
+
Yasm::Manager.execute(
|
24
|
+
:context => context,
|
25
|
+
:state_container => self,
|
26
|
+
:actions => actions
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/yasm/manager.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Yasm
|
2
|
+
module Manager
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def change_state(options)
|
6
|
+
new_state = options[:to]
|
7
|
+
state_container = options[:on]
|
8
|
+
new_state = new_state.to_class if new_state.respond_to? :to_class
|
9
|
+
|
10
|
+
state_container.state = new_state.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(options)
|
14
|
+
context = options[:context]
|
15
|
+
actions = options[:actions]
|
16
|
+
state_container = options[:state_container]
|
17
|
+
|
18
|
+
actions.each do |action|
|
19
|
+
action = action.new if action.class == Class
|
20
|
+
action.context = context
|
21
|
+
action.state_container = state_container
|
22
|
+
|
23
|
+
|
24
|
+
# Verify that the action is possible given the current state
|
25
|
+
if state_container.state.class.final?
|
26
|
+
raise "We're sorry, but the current state `#{state_container.state}` is final. It does not accept any actions."
|
27
|
+
elsif !state_container.state.class.is_allowed?(action.class)
|
28
|
+
raise "We're sorry, but the action `#{action.class}` is not possible given the current state `#{state_container.state}`."
|
29
|
+
end
|
30
|
+
|
31
|
+
change_state :to => action.triggers.to_class, :on => state_container if action.triggers
|
32
|
+
action.execute
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/yasm/state.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Yasm
|
2
|
+
module State
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def actions(*allowed_actions)
|
9
|
+
@allowed_actions = allowed_actions
|
10
|
+
end
|
11
|
+
|
12
|
+
def allowed_actions
|
13
|
+
@allowed_actions
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_allowed?(action)
|
17
|
+
return true if @allowed_actions.nil?
|
18
|
+
@allowed_actions.include? action.to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def final!
|
22
|
+
@allowed_actions = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def final?
|
26
|
+
@allowed_actions == []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
self.class.to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/yasm/version.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
$LOAD_PATH.unshift './lib'
|
2
|
+
require 'yasm'
|
3
|
+
|
4
|
+
# class VendingMachine
|
5
|
+
# include Yasm::Context
|
6
|
+
#
|
7
|
+
# start Waiting
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# class Waiting
|
11
|
+
# include Yasm::State
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# class Vending
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# class InputMoney
|
18
|
+
# include Yasm::Action
|
19
|
+
#
|
20
|
+
# triggers Vending
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
#
|
24
|
+
# class VendingMachine
|
25
|
+
# include Yasm::Context
|
26
|
+
#
|
27
|
+
# # start Waiting
|
28
|
+
# #
|
29
|
+
# # state :status do
|
30
|
+
# # start Waiting
|
31
|
+
# # end
|
32
|
+
# #
|
33
|
+
# attr_accessor :money
|
34
|
+
# def initialize
|
35
|
+
# @money = 0
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# class Waiting
|
40
|
+
# include Yasm::State
|
41
|
+
#
|
42
|
+
# actions InputMoney
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# class Vending
|
46
|
+
# include Yasm::State
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# class NotAState
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# class InputMoney
|
53
|
+
# include Yasm::Action
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# class Vend
|
57
|
+
# include Yasm::Action
|
58
|
+
# triggers Vending
|
59
|
+
#
|
60
|
+
# def initialize(item)
|
61
|
+
# @item = item
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# def execute
|
65
|
+
# puts "Vending #{@item}"
|
66
|
+
# context.do! Refund if context.money > 0
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# class Refund
|
71
|
+
# include Yasm::Action
|
72
|
+
#
|
73
|
+
# triggers Waiting
|
74
|
+
#
|
75
|
+
# def execute
|
76
|
+
# puts "Refunding #{context.money}"
|
77
|
+
# context.money = 0
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# class MakeSelection
|
82
|
+
# include Yasm::Action
|
83
|
+
#
|
84
|
+
# def initialize(selection)
|
85
|
+
# @selection = selection
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# def execute
|
89
|
+
# if context.money >= selection.price
|
90
|
+
# trigger Vending
|
91
|
+
# context.money -= selection.price
|
92
|
+
# context.do! Vend.new(selection)
|
93
|
+
# else
|
94
|
+
# puts "You must enter more money."
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# class NotAnAction
|
100
|
+
# end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Yasm::Context do
|
4
|
+
context "when included in a class" do
|
5
|
+
before do
|
6
|
+
class VendingMachine
|
7
|
+
include Yasm::Context
|
8
|
+
end
|
9
|
+
|
10
|
+
class Waiting
|
11
|
+
include Yasm::State
|
12
|
+
end
|
13
|
+
|
14
|
+
class InputMoney
|
15
|
+
include Yasm::Action
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "##state" do
|
20
|
+
it "should require something that can be converted to a symbol" do
|
21
|
+
proc { VendingMachine.state(nil) }.should raise_exception(ArgumentError, "The state name must respond to `to_sym`")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should create a new state configuration" do
|
25
|
+
VendingMachine.state_configurations[:electricity].should be_nil
|
26
|
+
class On; include Yasm::State; end
|
27
|
+
VendingMachine.state(:electricity) { start :on }
|
28
|
+
VendingMachine.state_configurations[:electricity].should_not be_nil
|
29
|
+
VendingMachine.state_configurations[:electricity].start_state.should == :on
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should instance_eval the block on the new state configuration" do
|
33
|
+
VendingMachine.state_configurations[:power].should be_nil
|
34
|
+
class On; include Yasm::State; end
|
35
|
+
VendingMachine.state(:power) { start :on }
|
36
|
+
VendingMachine.state_configurations[:power].should_not be_nil
|
37
|
+
VendingMachine.state_configurations[:power].start_state.should == :on
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should create an instance method that returns the state" do
|
41
|
+
class On; include Yasm::State; end
|
42
|
+
VendingMachine.state(:light) { start :on }
|
43
|
+
VendingMachine.new.light.state.class.should == On
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "##start" do
|
48
|
+
context "when called directly on the context" do
|
49
|
+
it "should store the state as the start state of the anonymous state configuration" do
|
50
|
+
VendingMachine.state_configurations[Yasm::Context::ANONYMOUS_STATE].should be_nil
|
51
|
+
VendingMachine.start :waiting
|
52
|
+
VendingMachine.state_configurations[Yasm::Context::ANONYMOUS_STATE].start_state.should == :waiting
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#do!" do
|
58
|
+
it "should pass all actions passed to it off to the anonymous state_container #do! method" do
|
59
|
+
v = VendingMachine.new
|
60
|
+
v.state.should_receive(:do!).with(InputMoney, InputMoney)
|
61
|
+
v.do! InputMoney, InputMoney
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Yasm::Conversions::Symbol do
|
4
|
+
it "should create a :to_class instance method on Symbols" do
|
5
|
+
:sym.respond_to?(:to_class).should be_true
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Symbol do
|
10
|
+
describe "#to_class" do
|
11
|
+
it "should convert the symbol to a class" do
|
12
|
+
class SymbolTest; end
|
13
|
+
:symbol_test.to_class.should == SymbolTest
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be the inverse of the Class #to_sym class method" do
|
17
|
+
class SymbolTest; end
|
18
|
+
:symbol_test.to_class.to_sym.should == :symbol_test
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
describe Yasm::Conversions::Class do
|
25
|
+
it "should create a :to_sym class method on classes" do
|
26
|
+
class SymbolTest; end
|
27
|
+
SymbolTest.respond_to?(:to_sym).should be_true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe Symbol do
|
32
|
+
describe "#to_sym" do
|
33
|
+
it "should convert the class to a symbol" do
|
34
|
+
class SymbolTest; end
|
35
|
+
SymbolTest.to_sym.should == :symbol_test
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should be the inverse of the Symbol #to_class instance method" do
|
39
|
+
class SymbolTest; end
|
40
|
+
SymbolTest.to_sym.to_class.should == SymbolTest
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Yasm::Manager do
|
4
|
+
describe "##execute" do
|
5
|
+
before do
|
6
|
+
class VendingMachine
|
7
|
+
include Yasm::Context
|
8
|
+
|
9
|
+
start :on
|
10
|
+
end
|
11
|
+
|
12
|
+
class Unplug
|
13
|
+
include Yasm::Action
|
14
|
+
|
15
|
+
triggers :off
|
16
|
+
end
|
17
|
+
|
18
|
+
class PlugIn
|
19
|
+
include Yasm::Action
|
20
|
+
|
21
|
+
triggers :on
|
22
|
+
end
|
23
|
+
|
24
|
+
class Destroy
|
25
|
+
include Yasm::Action
|
26
|
+
|
27
|
+
triggers :destroyed
|
28
|
+
end
|
29
|
+
|
30
|
+
class On
|
31
|
+
include Yasm::State
|
32
|
+
|
33
|
+
actions :unplug, :destroy
|
34
|
+
end
|
35
|
+
|
36
|
+
class Off
|
37
|
+
include Yasm::State
|
38
|
+
|
39
|
+
actions :plug_in, :destroy
|
40
|
+
end
|
41
|
+
|
42
|
+
class Destroyed
|
43
|
+
include Yasm::State
|
44
|
+
|
45
|
+
final!
|
46
|
+
end
|
47
|
+
|
48
|
+
@vending_machine = VendingMachine.new
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should apply each action, sequentially, to the appropriate state_container within the context" do
|
52
|
+
@vending_machine.state.value.class.should == On
|
53
|
+
|
54
|
+
Yasm::Manager.execute :context => @vending_machine, :state_container => @vending_machine.state, :actions => [Unplug]
|
55
|
+
@vending_machine.state.value.class.should == Off
|
56
|
+
|
57
|
+
Yasm::Manager.execute :context => @vending_machine, :state_container => @vending_machine.state, :actions => [PlugIn]
|
58
|
+
@vending_machine.state.value.class.should == On
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should verify that the action is possible given the current state" do
|
62
|
+
@vending_machine.state.value.class.should == On
|
63
|
+
|
64
|
+
proc {
|
65
|
+
Yasm::Manager.execute(
|
66
|
+
:context => @vending_machine,
|
67
|
+
:state_container => @vending_machine.state,
|
68
|
+
:actions => [PlugIn]
|
69
|
+
)
|
70
|
+
}.should raise_exception("We're sorry, but the action `PlugIn` is not possible given the current state `On`.")
|
71
|
+
|
72
|
+
proc {
|
73
|
+
Yasm::Manager.execute(
|
74
|
+
:context => @vending_machine,
|
75
|
+
:state_container => @vending_machine.state,
|
76
|
+
:actions => [Unplug]
|
77
|
+
)
|
78
|
+
}.should_not raise_exception
|
79
|
+
|
80
|
+
proc {
|
81
|
+
Yasm::Manager.execute(
|
82
|
+
:context => @vending_machine,
|
83
|
+
:state_container => @vending_machine.state,
|
84
|
+
:actions => [Destroy, PlugIn]
|
85
|
+
)
|
86
|
+
}.should raise_exception("We're sorry, but the current state `Destroyed` is final. It does not accept any actions.")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Yasm::Context::StateContainer do
|
4
|
+
before do
|
5
|
+
class VendingMachine
|
6
|
+
include Yasm::Context
|
7
|
+
end
|
8
|
+
|
9
|
+
class Waiting
|
10
|
+
include Yasm::State
|
11
|
+
end
|
12
|
+
|
13
|
+
class InputMoney
|
14
|
+
include Yasm::Action
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#do!" do
|
19
|
+
it "should pass the actions off to the Yasm::Manager.execute method" do
|
20
|
+
v = VendingMachine.new
|
21
|
+
Yasm::Manager.should_receive(:execute).with(:context => v, :state_container => v.state, :actions => [InputMoney, InputMoney])
|
22
|
+
v.state.do! InputMoney, InputMoney
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Yasm::State do
|
4
|
+
before do
|
5
|
+
class TestState
|
6
|
+
include Yasm::State
|
7
|
+
end
|
8
|
+
|
9
|
+
class Action1; include Yasm::Action; end
|
10
|
+
class Action2; include Yasm::Action; end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "##actions" do
|
14
|
+
it "should set the @allowed_actions to the input to this method" do
|
15
|
+
TestState.actions Action1, Action2
|
16
|
+
TestState.allowed_actions.should == [Action1, Action2]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "##final!" do
|
21
|
+
it "should set the @allowed_actions to an empty array" do
|
22
|
+
TestState.final!
|
23
|
+
TestState.allowed_actions.should == []
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "##final?" do
|
28
|
+
it "should return true if there are no allowed actions" do
|
29
|
+
TestState.final!
|
30
|
+
TestState.final?.should be_true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/yasm.gemspec
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{yasm}
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Matt Parker"]
|
12
|
+
s.date = %q{2011-02-12}
|
13
|
+
s.description = %q{Breaks up states, actions, and contexts into seperate classes.moonmaster9000@gmail.com}
|
14
|
+
s.email = %q{moonmaster9000@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.markdown"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"VERSION",
|
20
|
+
"lib/yasm.rb",
|
21
|
+
"lib/yasm/action.rb",
|
22
|
+
"lib/yasm/context.rb",
|
23
|
+
"lib/yasm/context/anonymous_state_identifier.rb",
|
24
|
+
"lib/yasm/context/state_configuration.rb",
|
25
|
+
"lib/yasm/context/state_container.rb",
|
26
|
+
"lib/yasm/conversions.rb",
|
27
|
+
"lib/yasm/conversions/class.rb",
|
28
|
+
"lib/yasm/conversions/symbol.rb",
|
29
|
+
"lib/yasm/manager.rb",
|
30
|
+
"lib/yasm/state.rb",
|
31
|
+
"lib/yasm/version.rb",
|
32
|
+
"yasm.gemspec"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/moonmaster9000/yasm}
|
35
|
+
s.require_paths = ["lib"]
|
36
|
+
s.rubygems_version = %q{1.5.0}
|
37
|
+
s.summary = %q{Yet Another State Machine. Pronounced "yaz-um."}
|
38
|
+
s.test_files = [
|
39
|
+
"spec/spec_helper.rb",
|
40
|
+
"spec/yasm/context_spec.rb",
|
41
|
+
"spec/yasm/conversions_spec.rb",
|
42
|
+
"spec/yasm/manager_spec.rb",
|
43
|
+
"spec/yasm/state_container_spec.rb",
|
44
|
+
"spec/yasm/state_spec.rb"
|
45
|
+
]
|
46
|
+
|
47
|
+
if s.respond_to? :specification_version then
|
48
|
+
s.specification_version = 3
|
49
|
+
|
50
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
51
|
+
s.add_runtime_dependency(%q<activesupport>, ["~> 3.0"])
|
52
|
+
s.add_runtime_dependency(%q<i18n>, ["~> 0.5.0"])
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<activesupport>, ["~> 3.0"])
|
55
|
+
s.add_dependency(%q<i18n>, ["~> 0.5.0"])
|
56
|
+
end
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<activesupport>, ["~> 3.0"])
|
59
|
+
s.add_dependency(%q<i18n>, ["~> 0.5.0"])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yasm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matt Parker
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-02-12 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activesupport
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
version: "3.0"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: i18n
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 11
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
- 5
|
48
|
+
- 0
|
49
|
+
version: 0.5.0
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
description: Breaks up states, actions, and contexts into seperate classes.moonmaster9000@gmail.com
|
53
|
+
email: moonmaster9000@gmail.com
|
54
|
+
executables: []
|
55
|
+
|
56
|
+
extensions: []
|
57
|
+
|
58
|
+
extra_rdoc_files:
|
59
|
+
- README.markdown
|
60
|
+
files:
|
61
|
+
- VERSION
|
62
|
+
- lib/yasm.rb
|
63
|
+
- lib/yasm/action.rb
|
64
|
+
- lib/yasm/context.rb
|
65
|
+
- lib/yasm/context/anonymous_state_identifier.rb
|
66
|
+
- lib/yasm/context/state_configuration.rb
|
67
|
+
- lib/yasm/context/state_container.rb
|
68
|
+
- lib/yasm/conversions.rb
|
69
|
+
- lib/yasm/conversions/class.rb
|
70
|
+
- lib/yasm/conversions/symbol.rb
|
71
|
+
- lib/yasm/manager.rb
|
72
|
+
- lib/yasm/state.rb
|
73
|
+
- lib/yasm/version.rb
|
74
|
+
- yasm.gemspec
|
75
|
+
- README.markdown
|
76
|
+
- spec/spec_helper.rb
|
77
|
+
- spec/yasm/context_spec.rb
|
78
|
+
- spec/yasm/conversions_spec.rb
|
79
|
+
- spec/yasm/manager_spec.rb
|
80
|
+
- spec/yasm/state_container_spec.rb
|
81
|
+
- spec/yasm/state_spec.rb
|
82
|
+
has_rdoc: true
|
83
|
+
homepage: http://github.com/moonmaster9000/yasm
|
84
|
+
licenses: []
|
85
|
+
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
hash: 3
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
hash: 3
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
requirements: []
|
110
|
+
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 1.5.0
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Yet Another State Machine. Pronounced "yaz-um."
|
116
|
+
test_files:
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
- spec/yasm/context_spec.rb
|
119
|
+
- spec/yasm/conversions_spec.rb
|
120
|
+
- spec/yasm/manager_spec.rb
|
121
|
+
- spec/yasm/state_container_spec.rb
|
122
|
+
- spec/yasm/state_spec.rb
|