yasm 0.0.3 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ Given /^a context$/ do
2
+ class ActionFeatureContext
3
+ include Yasm::Context
4
+
5
+ start :action_feature_state_1
6
+ end
7
+
8
+ class ActionFeatureState1
9
+ include Yasm::State
10
+ end
11
+
12
+ class ActionFeatureState2
13
+ include Yasm::State
14
+ end
15
+
16
+ @context = ActionFeatureContext.new
17
+ @context.state.value.class.should == ActionFeatureState1
18
+ end
19
+
20
+ When /^I apply an action to it that triggers a state transition$/ do
21
+ class TriggerActionFeatureState2
22
+ include Yasm::Action
23
+
24
+ triggers :action_feature_state_2
25
+ end
26
+ @context.do! TriggerActionFeatureState2
27
+ end
28
+
29
+ Then /^the state should transition appropriately$/ do
30
+ @context.state.value.class.should == ActionFeatureState2
31
+ end
@@ -0,0 +1,34 @@
1
+ Given /^a state that I intend to declare final$/ do
2
+ class FinalState; include Yasm::State; end
3
+ FinalState.final?.should be_false
4
+ end
5
+
6
+ When /^I declare it final$/ do
7
+ FinalState.final!
8
+ end
9
+
10
+ Then /^it should return true when asked if it's a FINAL state$/ do
11
+ FinalState.final?.should be_true
12
+ end
13
+
14
+ Given /^a final state$/ do
15
+ class ContextWithFinalState
16
+ include Yasm::Context
17
+ start :final_state
18
+ end
19
+
20
+ class FinalState
21
+ include Yasm::State
22
+ final!
23
+ end
24
+ @context = ContextWithFinalState.new
25
+ end
26
+
27
+ When /^I attempt to apply an action to it$/ do
28
+ class ImpossibleAction; include Yasm::Action; end
29
+ @apply_action = proc {@context.do! ImpossibleAction}
30
+ end
31
+
32
+ Then /^I should get an exception$/ do
33
+ @apply_action.should raise_exception(Yasm::FinalStateException)
34
+ end
@@ -0,0 +1,85 @@
1
+ Given /^a class that inherits from CouchRest::Model::Base$/ do
2
+ class TestYasmCouchPersistence < CouchRest::Model::Base
3
+ end
4
+ end
5
+
6
+ When /^I mix Yasm::Context into it$/ do
7
+ class TestYasmCouchPersistence
8
+ include Yasm::Context
9
+ end
10
+ end
11
+
12
+ Then /^Yasm::Persistence::CouchRest::Model should be autoincluded as well$/ do
13
+ TestYasmCouchPersistence.ancestors.should be_include(Yasm::Persistence::CouchRest::Model)
14
+ end
15
+
16
+ Given /^a couchrest model context$/ do
17
+ class CouchContext < CouchRest::Model::Base
18
+ use_database YASM_COUCH_DB
19
+ include Yasm::Context
20
+
21
+ start :couch_state1
22
+ end
23
+ class CouchState1
24
+ include Yasm::State
25
+ end
26
+ class CouchState2
27
+ include Yasm::State
28
+ end
29
+ class GoToState2
30
+ include Yasm::Action
31
+ triggers :couch_state2
32
+ end
33
+ @couch_context = CouchContext.new
34
+ end
35
+
36
+ When /^I save that context to CouchDB$/ do
37
+ @couch_context.do! GoToState2
38
+ @state_start_time = @couch_context.state.value.instantiated_at
39
+ @couch_context.save
40
+ end
41
+
42
+ Then /^the states should be saved in the document$/ do
43
+ @doc = YASM_COUCH_DB.get @couch_context.id
44
+ @doc["yasm"].should_not be_nil
45
+ @doc["yasm"]["states"][Yasm::Context::ANONYMOUS_STATE.to_s].should_not be_nil
46
+ @doc["yasm"]["states"][Yasm::Context::ANONYMOUS_STATE.to_s]["class"].should == "CouchState2"
47
+ Time.parse(@doc["yasm"]["states"][Yasm::Context::ANONYMOUS_STATE.to_s]["instantiated_at"]).to_s.should == @state_start_time.to_s
48
+ end
49
+
50
+ Given /^a couchrest model context with state saved in the database$/ do
51
+ class CouchContext < CouchRest::Model::Base
52
+ include Yasm::Context
53
+
54
+ start :couch_state1
55
+ end
56
+ class CouchState1; include Yasm::State; end
57
+ class CouchState2
58
+ include Yasm::State
59
+
60
+ maximum 9.minutes, :action => :go_to_state3
61
+ end
62
+ class CouchState3
63
+ include Yasm::State
64
+ end
65
+ class GoToState2; include Yasm::Action; triggers :couch_state2; end
66
+ class GoToState3; include Yasm::Action; triggers :couch_state3; end
67
+ @couch_context = CouchContext.new
68
+ @couch_context.do! GoToState2
69
+ @state_start_time = @couch_context.state.value.instantiated_at
70
+ @couch_context.save
71
+ end
72
+
73
+ When /^I load that context$/ do
74
+ ten_minutes_from_now = 10.minutes.from_now
75
+ Time.stub(:now).and_return ten_minutes_from_now
76
+ @couch_context = CouchContext.find @couch_context.id
77
+ end
78
+
79
+ Then /^the states should be restored$/ do
80
+ @couch_context.state.value.class.should == CouchState3
81
+ end
82
+
83
+ Then /^the states should be fast forwarded$/ do
84
+ @couch_context.state.value.class.should == CouchState3
85
+ end
@@ -0,0 +1,27 @@
1
+ Given /^a state$/ do
2
+ class StateWithLimitedActions
3
+ include Yasm::State
4
+ end
5
+ end
6
+
7
+ When /^I declare that only certain actions can be applied to it$/ do
8
+ StateWithLimitedActions.actions :limited_action1, :limited_action2
9
+ end
10
+
11
+ Then /^those actions should be stored on the state class$/ do
12
+ StateWithLimitedActions.allowed_actions.should == [:limited_action1, :limited_action2]
13
+ end
14
+
15
+ Given /^a state with limited actions$/ do
16
+ StateWithLimitedActions.actions :limited_action1, :limited_action2
17
+ end
18
+
19
+ Then /^I should be able to determine whether a given action is valid for a given state$/ do
20
+ class LimitedAction1; include Yasm::Action; end
21
+ class LimitedAction2; include Yasm::Action; end
22
+ class LimitedAction3; include Yasm::Action; end
23
+
24
+ StateWithLimitedActions.is_allowed?(LimitedAction1).should be_true
25
+ StateWithLimitedActions.is_allowed?(LimitedAction2).should be_true
26
+ StateWithLimitedActions.is_allowed?(LimitedAction3).should be_false
27
+ end
@@ -0,0 +1,147 @@
1
+ Given /^a state that I intend to declare a minimum time limit on$/ do
2
+ class StateWithMinimumTimeLimit
3
+ include Yasm::State
4
+ end
5
+ StateWithMinimumTimeLimit.minimum_duration.should be_nil
6
+ end
7
+
8
+ When /^I declare a minimum time limit on it$/ do
9
+ StateWithMinimumTimeLimit.minimum 10.minutes
10
+ end
11
+
12
+ Then /^it should store that time limit on the state class$/ do
13
+ StateWithMinimumTimeLimit.minimum_duration.should == 10.minutes
14
+ end
15
+
16
+ Given /^a state that has a minimum time limit$/ do
17
+ class ContextWithStateWithMinimumTimeLimit
18
+ include Yasm::Context
19
+ start :state_with_minimum_time_limit
20
+ end
21
+
22
+ class StateWithMinimumTimeLimit
23
+ include Yasm::State
24
+ minimum 10.minutes
25
+ end
26
+
27
+ @context = ContextWithStateWithMinimumTimeLimit.new
28
+ end
29
+
30
+ When /^I apply an action to that state before its minimum time limit has been reached$/ do
31
+ class ActionOnStateWithMinimumTimeLimit
32
+ include Yasm::Action
33
+ end
34
+
35
+ @apply_action = proc {@context.do! ActionOnStateWithMinimumTimeLimit}
36
+ end
37
+
38
+ Then /^I should get a minimum time limit exception$/ do
39
+ @apply_action.should raise_exception(Yasm::TimeLimitNotYetReached)
40
+ end
41
+
42
+ When /^I apply an action to that state after its minimum time limit has been reached$/ do
43
+ @apply_action = proc {
44
+ ten_minutes_from_now = 10.minutes.from_now
45
+ Time.stub(:now).and_return ten_minutes_from_now
46
+ @context.do! ActionOnStateWithMinimumTimeLimit
47
+ }
48
+ end
49
+
50
+ Then /^I should not get a minimum time limit exception$/ do
51
+ @apply_action.should_not raise_exception
52
+ end
53
+
54
+ Given /^a state that I intend to declare a maximum time limit on$/ do
55
+ class StateWithMaxTimeLimit
56
+ include Yasm::State
57
+ end
58
+ end
59
+
60
+ When /^I declare a maximum time limit on it without an action$/ do
61
+ @declaration = proc {
62
+ StateWithMaxTimeLimit.maximum 10.minutes
63
+ }
64
+ end
65
+
66
+ Then /^it should raise an argument error exception$/ do
67
+ @declaration.should raise_error(ArgumentError)
68
+ end
69
+
70
+ When /^I declare a maximum time limit on it with an action$/ do
71
+ @declaration = proc {
72
+ StateWithMaxTimeLimit.maximum 10.minutes, :action => :max_time_limit_action
73
+ }
74
+ class MaxTimeLimitAction; include Yasm::Action; end
75
+ end
76
+
77
+ Then /^it should store that maximum time limit and action on the state class$/ do
78
+ @declaration.should_not raise_error
79
+ StateWithMaxTimeLimit.maximum_duration.should == 10.minutes
80
+ StateWithMaxTimeLimit.maximum_duration_action.should == MaxTimeLimitAction
81
+ end
82
+
83
+ Given /^a context with the potential for a max time limit dominoe effect$/ do
84
+ class DominoContext
85
+ include Yasm::Context
86
+
87
+ start :domino1
88
+ end
89
+
90
+ class Domino1
91
+ include Yasm::State
92
+
93
+ actions :trigger_domino2
94
+ maximum 10.minutes, :action => :trigger_domino2
95
+ end
96
+
97
+ class TriggerDomino2
98
+ include Yasm::Action
99
+
100
+ triggers :domino2
101
+ end
102
+
103
+ class Domino2
104
+ include Yasm::State
105
+
106
+ maximum 10.minutes, :action => :trigger_domino3
107
+ end
108
+
109
+ class TriggerDomino3
110
+ include Yasm::Action
111
+
112
+ triggers :domino3
113
+ end
114
+
115
+ class Domino3
116
+ include Yasm::State
117
+ end
118
+
119
+ class TriggerDomino4
120
+ include Yasm::Action
121
+
122
+ triggers :domino4
123
+ end
124
+
125
+ class Domino4
126
+ include Yasm::State
127
+ end
128
+ end
129
+
130
+ When /^I wait long enough to cause the dominoe effect$/ do
131
+ @wait = proc {
132
+ @context = DominoContext.new
133
+ @context.state.value
134
+ thirty_minutes_from_now = 30.minutes.from_now
135
+ Time.stub(:now).and_return thirty_minutes_from_now
136
+ }
137
+ end
138
+
139
+ Then /^the dominoe effect should occur when I ask for the state$/ do
140
+ @wait.call
141
+ @context.state.value.class.should == Domino3
142
+ end
143
+
144
+ Then /^the dominoe effect should occur when I attempt to apply an action to the context$/ do
145
+ @context.do! TriggerDomino4
146
+ @context.state.value.class.should == Domino4
147
+ end
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift './lib'
2
+ require 'yasm'
3
+ require 'rspec'
4
+ require 'rspec/mocks/standalone'
5
+ require 'couchrest_model'
6
+
7
+ COUCHDB_SERVER = CouchRest.new "http://admin:password@localhost:5984"
8
+ YASM_COUCH_DB = COUCHDB_SERVER.database!('yasm_test')
9
+ COUCHDB_SERVER.default_database = 'yasm_test'
10
+
11
+ Before('@couch') do
12
+ YASM_COUCH_DB.recreate!
13
+ end
@@ -9,4 +9,6 @@ require 'yasm/action'
9
9
  require 'yasm/context/anonymous_state_identifier'
10
10
  require 'yasm/context/state_configuration'
11
11
  require 'yasm/context/state_container'
12
+ require 'yasm/context/state_configuration/action_hook'
12
13
  require 'yasm/context'
14
+ require 'yasm/persistence'
@@ -2,23 +2,32 @@ module Yasm
2
2
  module Context
3
3
  def self.included(base)
4
4
  base.extend ClassMethods
5
+ if defined?(CouchRest) and defined?(CouchRest::Model) and base.ancestors.include?(CouchRest::Model::Base) and !base.ancestors.include?(Yasm::Persistence::CouchRest::Model)
6
+ base.send :include, Yasm::Persistence::CouchRest::Model
7
+ end
5
8
  end
6
9
 
7
10
  module ClassMethods
11
+ def after_action(method, options={})
12
+ state_configuration(ANONYMOUS_STATE).after_action method, options
13
+ end
14
+
15
+ def before_action(method, options={})
16
+ state_configuration(ANONYMOUS_STATE).before_action method, options
17
+ end
18
+
8
19
  # for a simple, anonymous state
9
20
  def start(state)
10
- state_configurations[ANONYMOUS_STATE] = StateConfiguration.new
11
- state_configurations[ANONYMOUS_STATE].start state
21
+ state_configuration(ANONYMOUS_STATE).start state
12
22
  end
13
23
 
14
24
  # for a named state
15
25
  def state(name, &block)
16
26
  raise ArgumentError, "The state name must respond to `to_sym`" unless name.respond_to?(:to_sym)
17
27
  name = name.to_sym
18
- state_configurations[name] = StateConfiguration.new
19
- state_configurations[name].instance_eval &block
28
+ state_configuration(name).instance_eval &block
20
29
 
21
- raise "You must provide a start state for #{name}" unless state_configurations[name].start_state
30
+ raise "You must provide a start state for #{name}" unless state_configuration(name).start_state
22
31
 
23
32
  define_method(name) { state_container name }
24
33
  end
@@ -27,6 +36,11 @@ module Yasm
27
36
  def state_configurations
28
37
  @state_configurations ||= {}
29
38
  end
39
+
40
+ private
41
+ def state_configuration(name)
42
+ state_configurations[name] ||= StateConfiguration.new
43
+ end
30
44
  end
31
45
 
32
46
  def do!(*actions)
@@ -38,6 +52,10 @@ module Yasm
38
52
  state_container ANONYMOUS_STATE
39
53
  end
40
54
 
55
+ def fast_forward
56
+ self.class.state_configurations.keys.each { |state_name| state_container(state_name).value }
57
+ end
58
+
41
59
  private
42
60
  def state_containers
43
61
  @state_containers ||= {}
@@ -45,7 +63,7 @@ module Yasm
45
63
 
46
64
  def state_container(id)
47
65
  unless state_containers[id]
48
- state_containers[id] = StateContainer.new :context => self
66
+ state_containers[id] = StateContainer.new :context => self, :name => id
49
67
  Yasm::Manager.change_state :to => self.class.state_configurations[id].start_state, :on => state_containers[id]
50
68
  end
51
69
 
@@ -1,15 +1,25 @@
1
1
  module Yasm
2
2
  module Context
3
3
  class StateConfiguration
4
- attr_reader :start_state
5
-
4
+ attr_reader :start_state, :after_actions, :before_actions
5
+
6
6
  def initialize(start_state = nil)
7
7
  @start_state = start_state
8
+ @after_actions = []
9
+ @before_actions = []
8
10
  end
9
11
 
10
12
  def start(state)
11
13
  @start_state = state
12
14
  end
15
+
16
+ def after_action(method, options={})
17
+ @after_actions << ActionHook.new(method, options)
18
+ end
19
+
20
+ def before_action(method, options={})
21
+ @before_actions << ActionHook.new(method, options)
22
+ end
13
23
  end
14
24
  end
15
25
  end