yasm 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -93,7 +93,7 @@ invalid actions to the context, `Yasm` will raise an exception.
93
93
  #==> Waiting
94
94
 
95
95
  vending_machine.do! RetrieveSelection
96
- #==> RuntimeError: We're sorry, but the action `RetrieveSelection`
96
+ #==> InvalidActionException: We're sorry, but the action `RetrieveSelection`
97
97
  is not possible given the current state `Waiting`.
98
98
 
99
99
  vending_machine.do! InputMoney
@@ -101,7 +101,8 @@ invalid actions to the context, `Yasm` will raise an exception.
101
101
  vending_machine.state.value
102
102
  #==> Waiting
103
103
 
104
- ## Side Effects
104
+
105
+ ### Side Effects
105
106
 
106
107
  How can we take our simulation farther? A real vending machine would verify that when you make a selection,
107
108
  you actually have input enough money to pay for that selection. How can we model this?
@@ -197,7 +198,7 @@ pay for our selection).
197
198
  #==> Waiting
198
199
 
199
200
 
200
- ## End states
201
+ ### End states
201
202
 
202
203
  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
204
  to work again after that. You can use the `final!` macro on a state to denote that this is the end.
@@ -219,8 +220,67 @@ to work again after that. You can use the `final!` macro on a state to denote th
219
220
  vending_machine.do! TossOffBuilding
220
221
 
221
222
  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
+ #==> Yasm::FinalStateException: We're sorry, but the current state `Obliterated` is final. It does not accept any actions.
224
+
225
+
226
+ ### State Timers
227
+
228
+ When a vending machine vends an item, it takes about 10 seconds for the item to work it's way off the rack and fall to the bottom. We can simulate this
229
+ by placing a `minimum` constraint on the `Vending` state.
230
+
231
+ class Vending
232
+ include Yasm::State
233
+
234
+ minimum 10.seconds
235
+ end
236
+
237
+
238
+ Now, when we go into the vending state, we won't be able to retrieve our selection until 10 seconds have passed.
239
+
240
+ vending_machine.do! MakeSelection.new(SnickersBar)
241
+
242
+ vending_machine.state.value
243
+ #==> Vending
244
+
245
+ vending_machine.do! RetrieveSelection
246
+ #==> Yasm::TimeLimitNotYetReached: We're sorry, but the time limit on the state `Vending` has not yet been reached.
247
+
248
+ sleep 10
249
+
250
+ vending_machine.do! RetrieveSelection
251
+
252
+ vending_machine.state.value
253
+ #==> Waiting
254
+
255
+ You can also create maximum time limits. For example, suppose we want our vending machine to self destruct, out of frustration, if it goes
256
+ an entire minute without any action.
257
+
258
+ class Waiting
259
+ include Yasm::State
260
+
261
+ maximum 1.minute, :action => :self_destruct
262
+ end
263
+
264
+ class SelfDestruct
265
+ include Yasm::Action
266
+
267
+ triggers :obliterated
223
268
 
269
+ def execute
270
+ puts "KABOOM!"
271
+ end
272
+ end
273
+
274
+ Now, if we create a vending machine, then wait at least a minute, next time we try to do something to it, it will execute the `SelfDestruct` action.
275
+
276
+
277
+ v = VendingMachine.new
278
+
279
+ sleep 60
280
+
281
+ v.do! InputMoney.new(10)
282
+ #==> "KABOOM!"
283
+ #==> Yasm::FinalStateException: We're sorry, but the current state `Obliterated` is final. It does not accept any actions.
224
284
 
225
285
 
226
286
  ## PUBLIC DOMAIN
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
data/lib/yasm.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'active_support/core_ext/string'
2
+ require 'active_support/time'
3
+ require 'yasm/exceptions'
2
4
  require 'yasm/conversions'
3
5
  require 'yasm/manager'
4
6
  require 'yasm/version'
data/lib/yasm/context.rb CHANGED
@@ -45,11 +45,8 @@ module Yasm
45
45
 
46
46
  def state_container(id)
47
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
- )
48
+ state_containers[id] = StateContainer.new :context => self
49
+ Yasm::Manager.change_state :to => self.class.state_configurations[id].start_state, :on => state_containers[id]
53
50
  end
54
51
 
55
52
  state_containers[id]
@@ -20,6 +20,8 @@ module Yasm
20
20
  end
21
21
 
22
22
  def do!(*actions)
23
+ actions = [self.state.class.maximum_duration_action] + actions if self.state.passed_maximum_time_limit?
24
+
23
25
  Yasm::Manager.execute(
24
26
  :context => context,
25
27
  :state_container => self,
@@ -0,0 +1,3 @@
1
+ require 'yasm/exceptions/time_limit_not_yet_reached'
2
+ require 'yasm/exceptions/final_state_exception'
3
+ require 'yasm/exceptions/invalid_action_exception'
@@ -0,0 +1,3 @@
1
+ module Yasm
2
+ class FinalStateException < RuntimeError; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module Yasm
2
+ class InvalidActionException < RuntimeError; end
3
+ end
@@ -0,0 +1,4 @@
1
+ module Yasm
2
+ class TimeLimitNotYetReached < RuntimeError
3
+ end
4
+ end
data/lib/yasm/manager.rb CHANGED
@@ -5,9 +5,17 @@ module Yasm
5
5
  def change_state(options)
6
6
  new_state = options[:to]
7
7
  state_container = options[:on]
8
+
9
+ raise(
10
+ Yasm::TimeLimitNotYetReached,
11
+ "We're sorry, but the time limit on the state `#{state_container.state}` has not yet been reached."
12
+ ) if state_container.state and !state_container.state.reached_minimum_time_limit?
13
+
8
14
  new_state = new_state.to_class if new_state.respond_to? :to_class
15
+ new_state = new_state.new
16
+ new_state.instantiated_at = Time.now
9
17
 
10
- state_container.state = new_state.new
18
+ state_container.state = new_state
11
19
  end
12
20
 
13
21
  def execute(options)
@@ -23,9 +31,9 @@ module Yasm
23
31
 
24
32
  # Verify that the action is possible given the current state
25
33
  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."
34
+ raise Yasm::FinalStateException, "We're sorry, but the current state `#{state_container.state}` is final. It does not accept any actions."
27
35
  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}`."
36
+ raise Yasm::InvalidActionException, "We're sorry, but the action `#{action.class}` is not possible given the current state `#{state_container.state}`."
29
37
  end
30
38
 
31
39
  change_state :to => action.triggers.to_class, :on => state_container if action.triggers
data/lib/yasm/state.rb CHANGED
@@ -25,10 +25,49 @@ module Yasm
25
25
  def final?
26
26
  @allowed_actions == []
27
27
  end
28
+
29
+ def minimum(time)
30
+ raise(
31
+ ArgumentError,
32
+ "You must provide a Fixnum to the ##minimum method (represents number of seconds). For example: 2.minutes"
33
+ ) unless time.kind_of?(Fixnum)
34
+
35
+ @state_minimum_duration = time
36
+ end
37
+
38
+ def minimum_duration
39
+ @state_minimum_duration
40
+ end
41
+
42
+ def maximum(maximum_duration, action_hash)
43
+ @maximum_duration = maximum_duration
44
+ @maximum_duration_action = action_hash[:action]
45
+ end
46
+
47
+ def maximum_duration
48
+ @maximum_duration
49
+ end
50
+
51
+ def maximum_duration_action
52
+ return @maximum_duration_action.call if @maximum_duration_action.respond_to? :call
53
+ @maximum_duration_action.to_class
54
+ end
28
55
  end
56
+
57
+ attr_accessor :instantiated_at
29
58
 
30
59
  def to_s
31
60
  self.class.to_s
32
61
  end
62
+
63
+ def reached_minimum_time_limit?
64
+ return true unless self.class.minimum_duration
65
+ (Time.now - instantiated_at) >= self.class.minimum_duration
66
+ end
67
+
68
+ def passed_maximum_time_limit?
69
+ return false unless self.class.maximum_duration
70
+ (Time.now - instantiated_at) >= self.class.maximum_duration
71
+ end
33
72
  end
34
73
  end
@@ -1,53 +1,82 @@
1
1
  require 'spec_helper'
2
2
 
3
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
4
+ before do
5
+ class VendingMachine
6
+ include Yasm::Context
7
+
8
+ start :on
9
+ end
11
10
 
12
- class Unplug
13
- include Yasm::Action
14
-
15
- triggers :off
16
- end
11
+ class Unplug
12
+ include Yasm::Action
13
+
14
+ triggers :off
15
+ end
17
16
 
18
- class PlugIn
19
- include Yasm::Action
20
-
21
- triggers :on
22
- end
17
+ class PlugIn
18
+ include Yasm::Action
19
+
20
+ triggers :on
21
+ end
23
22
 
24
- class Destroy
25
- include Yasm::Action
23
+ class Destroy
24
+ include Yasm::Action
26
25
 
27
- triggers :destroyed
28
- end
26
+ triggers :destroyed
27
+ end
29
28
 
30
- class On
31
- include Yasm::State
32
-
33
- actions :unplug, :destroy
34
- end
29
+ class On
30
+ include Yasm::State
31
+
32
+ actions :unplug, :destroy
33
+ end
35
34
 
36
- class Off
37
- include Yasm::State
38
-
39
- actions :plug_in, :destroy
40
- end
35
+ class Off
36
+ include Yasm::State
37
+
38
+ actions :plug_in, :destroy
39
+ end
40
+
41
+ class Destroyed
42
+ include Yasm::State
43
+
44
+ final!
45
+ end
46
+
47
+ @vending_machine = VendingMachine.new
48
+ end
49
+
41
50
 
42
- class Destroyed
51
+
52
+ describe "##change_state" do
53
+ it "should convert the state to a class if we pass an object that respond to :to_class" do
54
+ Destroyed.should_receive(:to_class).and_return Destroyed
55
+ Yasm::Manager.change_state :to => Destroyed, :on => @vending_machine.state
56
+ end
57
+
58
+ it "should set the instantiated_at property on the state to the current time" do
59
+ seconds_since_the_epoch = Time.now
60
+ Time.should_receive(:now).twice.and_return seconds_since_the_epoch
61
+ Yasm::Manager.change_state :to => On, :on => @vending_machine.state
62
+ @vending_machine.state.value.instantiated_at.should == seconds_since_the_epoch
63
+ end
64
+
65
+ it "should raise an exception if the current state has not yet reached it's time limit" do
66
+ class TenSeconds
43
67
  include Yasm::State
44
68
 
45
- final!
69
+ minimum 10.seconds
46
70
  end
47
-
48
- @vending_machine = VendingMachine.new
71
+
72
+ proc {
73
+ Yasm::Manager.change_state :to => TenSeconds, :on => @vending_machine.state
74
+ Yasm::Manager.change_state :to => On, :on => @vending_machine.state
75
+ }.should raise_exception(Yasm::TimeLimitNotYetReached, "We're sorry, but the time limit on the state `TenSeconds` has not yet been reached.")
49
76
  end
50
-
77
+ end
78
+
79
+ describe "##execute" do
51
80
  it "should apply each action, sequentially, to the appropriate state_container within the context" do
52
81
  @vending_machine.state.value.class.should == On
53
82
 
@@ -58,7 +87,7 @@ describe Yasm::Manager do
58
87
  @vending_machine.state.value.class.should == On
59
88
  end
60
89
 
61
- it "should verify that the action is possible given the current state" do
90
+ it "should raise an exception if you attempt to execute an action that isn't allowed by a state" do
62
91
  @vending_machine.state.value.class.should == On
63
92
 
64
93
  proc {
@@ -67,8 +96,20 @@ describe Yasm::Manager do
67
96
  :state_container => @vending_machine.state,
68
97
  :actions => [PlugIn]
69
98
  )
70
- }.should raise_exception("We're sorry, but the action `PlugIn` is not possible given the current state `On`.")
99
+ }.should raise_exception(Yasm::InvalidActionException, "We're sorry, but the action `PlugIn` is not possible given the current state `On`.")
100
+ end
71
101
 
102
+ it "should raise an exception if you attempt to execute an action on a final state." do
103
+ proc {
104
+ Yasm::Manager.execute(
105
+ :context => @vending_machine,
106
+ :state_container => @vending_machine.state,
107
+ :actions => [Destroy, PlugIn]
108
+ )
109
+ }.should raise_exception(Yasm::FinalStateException, "We're sorry, but the current state `Destroyed` is final. It does not accept any actions.")
110
+ end
111
+
112
+ it "should not raise an exception if the action is allowed by the state" do
72
113
  proc {
73
114
  Yasm::Manager.execute(
74
115
  :context => @vending_machine,
@@ -76,14 +117,6 @@ describe Yasm::Manager do
76
117
  :actions => [Unplug]
77
118
  )
78
119
  }.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
120
  end
88
121
  end
89
122
  end
@@ -4,11 +4,39 @@ describe Yasm::Context::StateContainer do
4
4
  before do
5
5
  class VendingMachine
6
6
  include Yasm::Context
7
+
8
+ start :waiting
7
9
  end
8
10
 
9
11
  class Waiting
10
12
  include Yasm::State
11
13
  end
14
+
15
+ class Hit
16
+ include Yasm::Action
17
+
18
+ triggers :jammed
19
+ end
20
+
21
+ class Jammed
22
+ include Yasm::State
23
+
24
+ maximum 10.seconds, :action => :explode
25
+ end
26
+
27
+ class Exploded
28
+ include Yasm::State
29
+
30
+ actions :clean_up
31
+ end
32
+
33
+ class CleanUp
34
+ include Yasm::Action
35
+ end
36
+
37
+ class Explode
38
+ include Yasm::Action
39
+ end
12
40
 
13
41
  class InputMoney
14
42
  include Yasm::Action
@@ -21,5 +49,14 @@ describe Yasm::Context::StateContainer do
21
49
  Yasm::Manager.should_receive(:execute).with(:context => v, :state_container => v.state, :actions => [InputMoney, InputMoney])
22
50
  v.state.do! InputMoney, InputMoney
23
51
  end
52
+
53
+ it "should add the maximum action to the front of the action list if the maximum time limit has been reached" do
54
+ v = VendingMachine.new
55
+ v.do! Hit
56
+ Yasm::Manager.should_receive(:execute).with(:context => v, :state_container => v.state, :actions => [Explode, CleanUp])
57
+ time = 10.seconds.from_now
58
+ Time.stub!(:now).and_return time
59
+ v.do! CleanUp
60
+ end
24
61
  end
25
62
  end
@@ -30,4 +30,81 @@ describe Yasm::State do
30
30
  TestState.final?.should be_true
31
31
  end
32
32
  end
33
+
34
+ describe "##minimum" do
35
+ it "should require an integer" do
36
+ proc { TestState.minimum "10 minutes" }.should raise_exception("You must provide a Fixnum to the ##minimum method (represents number of seconds). For example: 2.minutes")
37
+ end
38
+
39
+ it "should set the @state_minimum_duration to the number input" do
40
+ TestState.minimum 1.minute
41
+ TestState.minimum_duration.should == 60
42
+ end
43
+ end
44
+
45
+ describe "#reached_minimum_time_limit?" do
46
+ before do
47
+ class MinState
48
+ include Yasm::State
49
+ end
50
+ end
51
+
52
+ it "should return true if there is no minimum time limit for the state" do
53
+ MinState.new.reached_minimum_time_limit?.should be_true
54
+ end
55
+
56
+ it "should return false if there is a time limit that hasn't been reached yet" do
57
+ MinState.minimum 10.seconds
58
+ state = MinState.new
59
+ state.instantiated_at = Time.now
60
+ state.reached_minimum_time_limit?.should be_false
61
+ end
62
+
63
+ it "should return true if the state has reached it's time limit" do
64
+ MinState.minimum 10.seconds
65
+ state = MinState.new
66
+ state.instantiated_at = Time.now
67
+ ten_seconds_from_now = 10.seconds.from_now
68
+ Time.should_receive(:now).and_return ten_seconds_from_now
69
+ state.reached_minimum_time_limit?
70
+ end
71
+ end
72
+
73
+ describe "##maximum" do
74
+ it "should require both a time limit and an action" do
75
+ proc { TestState.maximum }.should raise_exception(ArgumentError)
76
+ proc { TestState.maximum 10.seconds }.should raise_exception(ArgumentError)
77
+ proc { TestState.maximum 10.seconds, :action => :action1 }.should_not raise_exception
78
+ end
79
+
80
+ it "should store the time limit and action" do
81
+ TestState.maximum 20.seconds, :action => :action2
82
+ TestState.maximum_duration.should == 20.seconds
83
+ TestState.maximum_duration_action.should == Action2
84
+ end
85
+ end
86
+
87
+ describe "##passed_maximum_time_limit?" do
88
+ it "should return false if no time limit has been set" do
89
+ class UnlimitedState
90
+ include Yasm::State
91
+ end
92
+
93
+ UnlimitedState.new.passed_maximum_time_limit?.should be_false
94
+ end
95
+
96
+ it "should return true if a time limit was set, and that limit has been passed" do
97
+ class LimitedState
98
+ include Yasm::State
99
+
100
+ maximum 10.seconds, :action => :action2
101
+ end
102
+
103
+ s = LimitedState.new
104
+ s.instantiated_at = Time.now
105
+ ten_seconds_from_now = 10.seconds.from_now
106
+ Time.stub!(:now).and_return ten_seconds_from_now
107
+ s.passed_maximum_time_limit?.should be_true
108
+ end
109
+ end
33
110
  end
data/yasm.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{yasm}
8
- s.version = "0.0.1"
8
+ s.version = "0.0.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Matt Parker"]
12
- s.date = %q{2011-02-12}
12
+ s.date = %q{2011-02-13}
13
13
  s.description = %q{Breaks up states, actions, and contexts into seperate classes.moonmaster9000@gmail.com}
14
14
  s.email = %q{moonmaster9000@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -26,6 +26,10 @@ Gem::Specification.new do |s|
26
26
  "lib/yasm/conversions.rb",
27
27
  "lib/yasm/conversions/class.rb",
28
28
  "lib/yasm/conversions/symbol.rb",
29
+ "lib/yasm/exceptions.rb",
30
+ "lib/yasm/exceptions/final_state_exception.rb",
31
+ "lib/yasm/exceptions/invalid_action_exception.rb",
32
+ "lib/yasm/exceptions/time_limit_not_yet_reached.rb",
29
33
  "lib/yasm/manager.rb",
30
34
  "lib/yasm/state.rb",
31
35
  "lib/yasm/version.rb",
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yasm
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Matt Parker
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-02-12 00:00:00 -05:00
18
+ date: 2011-02-13 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -68,6 +68,10 @@ files:
68
68
  - lib/yasm/conversions.rb
69
69
  - lib/yasm/conversions/class.rb
70
70
  - lib/yasm/conversions/symbol.rb
71
+ - lib/yasm/exceptions.rb
72
+ - lib/yasm/exceptions/final_state_exception.rb
73
+ - lib/yasm/exceptions/invalid_action_exception.rb
74
+ - lib/yasm/exceptions/time_limit_not_yet_reached.rb
71
75
  - lib/yasm/manager.rb
72
76
  - lib/yasm/state.rb
73
77
  - lib/yasm/version.rb