yasm 0.0.3 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/CHANGELOG +10 -0
- data/Gemfile +3 -0
- data/PUBLIC_DOMAIN +103 -0
- data/README.markdown +177 -0
- data/VERSION +1 -1
- data/features/action_callbacks.feature +22 -0
- data/features/actions.feature +9 -0
- data/features/final_states.feature +14 -0
- data/features/persistance/couchrest_model_persistance.feature +22 -0
- data/features/state_with_limited_actions.feature +13 -0
- data/features/states_with_time_limits.feature +29 -0
- data/features/step_definitions/action_callback_steps.rb +190 -0
- data/features/step_definitions/action_steps.rb +31 -0
- data/features/step_definitions/final_state_steps.rb +34 -0
- data/features/step_definitions/persistence/couchrest_model_persistence_steps.rb +85 -0
- data/features/step_definitions/state_with_limited_actions_steps.rb +27 -0
- data/features/step_definitions/states_with_time_limits_steps.rb +147 -0
- data/features/support/setup.rb +13 -0
- data/lib/yasm.rb +2 -0
- data/lib/yasm/context.rb +24 -6
- data/lib/yasm/context/state_configuration.rb +12 -2
- data/lib/yasm/context/state_configuration/action_hook.rb +33 -0
- data/lib/yasm/context/state_container.rb +29 -6
- data/lib/yasm/persistence.rb +1 -0
- data/lib/yasm/persistence/couchrest_model.rb +46 -0
- data/yasm.gemspec +12 -43
- metadata +69 -7
@@ -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
|
data/lib/yasm.rb
CHANGED
@@ -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'
|
data/lib/yasm/context.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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
|