yasm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|