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