yasm 0.0.1 → 0.0.2

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 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