stateful 0.0.2 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/stateful/mongoid.rb +21 -8
- data/lib/stateful/state_info.rb +9 -4
- data/lib/stateful/version.rb +1 -1
- data/lib/stateful.rb +61 -54
- data/spec/mongoid_spec.rb +45 -3
- data/spec/stateful_spec.rb +46 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43a52ed260948a8b25ab8e179b6cc1a4279243b8
|
4
|
+
data.tar.gz: 0099bcace08e9525183ff80bb81b4f8b3b32d6ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 01255d072732c11031920c57d6fbadc3c9242329ae4cb7ab4d07239e0a7cb2cebefa34cb7eedd7d4c73ed109764a14f64eb98300acbc34447a818383dce33009
|
7
|
+
data.tar.gz: ac27cc34184bbc0cd40676fd2cf1c198b61630080f2d6bd472bd2b30cdf7cdbee4dfe651df688e033d96cbf1085c3dd8326ef85107ab154bcc1ac936d3fea273
|
data/lib/stateful/mongoid.rb
CHANGED
@@ -1,26 +1,39 @@
|
|
1
1
|
module Stateful
|
2
|
-
module
|
2
|
+
module MongoidIntegration
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
module ClassMethods
|
6
6
|
protected
|
7
7
|
|
8
8
|
def define_state_attribute(options)
|
9
|
-
field :
|
10
|
-
validates_inclusion_of :
|
11
|
-
in:
|
12
|
-
message:
|
9
|
+
field options[:name].to_sym, type: Symbol, default: options[:default]
|
10
|
+
validates_inclusion_of options[:name].to_sym,
|
11
|
+
in: __send__("#{options[:name]}_infos").keys,
|
12
|
+
message: options.has_key?(:message) ? options[:message] : "has invalid value",
|
13
13
|
allow_nil: !!options[:allow_nil]
|
14
14
|
|
15
15
|
# configure scopes to query the attribute value
|
16
|
-
|
16
|
+
__send__("#{options[:name]}_infos").values.each do |info|
|
17
17
|
states = info.collect_child_states
|
18
|
+
scope_name = "#{options[:prefix]}#{info.name}"
|
18
19
|
if states.length == 1
|
19
|
-
scope
|
20
|
+
scope scope_name, where(options[:name] => states.first)
|
20
21
|
else
|
21
|
-
scope
|
22
|
+
scope scope_name, where(options[:name].to_sym.in => states)
|
22
23
|
end
|
23
24
|
end
|
25
|
+
|
26
|
+
# provide a previous_state helper since mongoid provides the state_change method for us
|
27
|
+
define_method "previous_#{options[:name]}" do
|
28
|
+
changes = __send__("#{options[:name]}_change")
|
29
|
+
changes.first if changes and changes.any?
|
30
|
+
end
|
31
|
+
|
32
|
+
define_method "previous_#{options[:name]}_info" do
|
33
|
+
state = __send__("previous_#{options[:name]}")
|
34
|
+
self.class.__send__("#{options[:name]}_infos")[state] if state
|
35
|
+
end
|
36
|
+
|
24
37
|
end
|
25
38
|
end
|
26
39
|
end
|
data/lib/stateful/state_info.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
module Stateful
|
2
2
|
class StateInfo
|
3
3
|
attr_reader :parent, :children, :name, :to_transitions
|
4
|
-
def initialize(state_class, parent, name, config)
|
4
|
+
def initialize(state_class, attr_name, parent, name, config)
|
5
|
+
@attr_name = attr_name
|
5
6
|
@state_class = state_class
|
6
7
|
if parent
|
7
8
|
@parent = parent
|
@@ -20,15 +21,19 @@ module Stateful
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def is?(state)
|
23
|
-
(@name == state or (parent and parent.is?(state)))
|
24
|
+
!!(@name == state or (parent and parent.is?(state)))
|
24
25
|
end
|
25
26
|
|
26
27
|
def is_group?
|
27
28
|
!!@groupConfig
|
28
29
|
end
|
29
30
|
|
31
|
+
def infos
|
32
|
+
@state_class.__send__("#{@attr_name}_infos")
|
33
|
+
end
|
34
|
+
|
30
35
|
def can_transition_to?(state)
|
31
|
-
state_info =
|
36
|
+
state_info = infos[state]
|
32
37
|
if is_group? or state_info.nil? or state_info.is_group?
|
33
38
|
false
|
34
39
|
else
|
@@ -43,7 +48,7 @@ module Stateful
|
|
43
48
|
def expand_to_transitions
|
44
49
|
if to_transitions.any?
|
45
50
|
@to_transitions = to_transitions.flat_map do |to|
|
46
|
-
info =
|
51
|
+
info = infos[to]
|
47
52
|
|
48
53
|
if info.is_group?
|
49
54
|
info.collect_child_states
|
data/lib/stateful/version.rb
CHANGED
data/lib/stateful.rb
CHANGED
@@ -9,42 +9,52 @@ module Stateful
|
|
9
9
|
if defined?(Mongoid)
|
10
10
|
require 'mongoid/document'
|
11
11
|
require 'stateful/mongoid'
|
12
|
-
include Stateful::
|
12
|
+
include Stateful::MongoidIntegration if included_modules.include?(::Mongoid::Document)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
17
|
-
def state_infos
|
18
|
-
@state_infos ||= {}
|
19
|
-
end
|
20
17
|
|
21
|
-
def stateful(options)
|
18
|
+
def stateful(name, options = nil)
|
19
|
+
if name.is_a?(Hash)
|
20
|
+
options = name
|
21
|
+
name = options[:name] || :state
|
22
|
+
end
|
23
|
+
|
24
|
+
options[:name] = name
|
25
|
+
|
22
26
|
options[:events] ||= []
|
27
|
+
options[:prefix] = name == :state ? '' : "#{name}_"
|
28
|
+
|
29
|
+
# define the method that will contain the info objects.
|
30
|
+
# we use instance_eval here because its easier to implement the ||= {} logic this way.
|
31
|
+
instance_eval "def #{name}_infos; @#{name}_infos ||= {}; end"
|
23
32
|
|
24
|
-
define_method
|
33
|
+
define_method "#{name}_events" do
|
25
34
|
options[:events]
|
26
35
|
end
|
27
36
|
|
28
|
-
define_method
|
29
|
-
self.class.
|
37
|
+
define_method "#{name}_info" do
|
38
|
+
self.class.__send__("#{name}_infos")[__send__(name)]
|
30
39
|
end
|
31
40
|
|
32
|
-
define_method
|
33
|
-
self.class.
|
41
|
+
define_method "#{name}_valid?" do
|
42
|
+
self.class.__send__("#{name}_infos").keys.include?(__send__(name))
|
34
43
|
end
|
35
44
|
|
36
|
-
define_method
|
37
|
-
return false if new_state ==
|
38
|
-
return false unless
|
39
|
-
|
45
|
+
define_method "change_#{name}" do |new_state, options = {}, &block|
|
46
|
+
return false if new_state == __send__(name)
|
47
|
+
return false unless __send__("#{name}_info").can_transition_to?(new_state)
|
48
|
+
__send__("_change_#{name}", new_state, options, [:persist_state, :save], &block)
|
40
49
|
end
|
41
50
|
|
42
|
-
define_method
|
43
|
-
|
44
|
-
|
51
|
+
define_method "change_#{name}!" do |new_state, options = {}, &block|
|
52
|
+
current_info = __send__("#{name}_info")
|
53
|
+
raise "transition from #{send(name)} to #{new_state} not allowed for #{name}" unless current_info.can_transition_to?(new_state)
|
54
|
+
__send__("_change_#{name}", new_state, options, [:persist_state!, :save!], &block)
|
45
55
|
end
|
46
56
|
|
47
|
-
define_method
|
57
|
+
define_method "_change_#{name}" do |new_state, options, persist_methods, &block|
|
48
58
|
# convert shortcut event name to options hash
|
49
59
|
options = {event: options} if options.is_a? Symbol
|
50
60
|
|
@@ -55,51 +65,48 @@ module Stateful
|
|
55
65
|
# options[:event] = calling_method if state_events.include? calling_method
|
56
66
|
#end
|
57
67
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
method = persist_methods.find {|m| respond_to?(m)}
|
73
|
-
__send__(method) if method
|
74
|
-
#end
|
68
|
+
callbacks = ["#{name}_change".to_sym]
|
69
|
+
callbacks << options[:event] if options[:event]
|
70
|
+
run_callbacks *callbacks do
|
71
|
+
__send__("#{name}=", new_state)
|
72
|
+
block.call if block
|
73
|
+
|
74
|
+
## if a specific persist method value was provided
|
75
|
+
if options.has_key?(:persist_method)
|
76
|
+
# call the method if one was provided
|
77
|
+
__send__(options[:persist_method]) if options[:persist_method]
|
78
|
+
# if no persist method option was provided than use the defaults
|
79
|
+
else
|
80
|
+
method = persist_methods.find {|m| respond_to?(m)}
|
81
|
+
__send__(method) if method
|
75
82
|
end
|
76
|
-
true
|
77
83
|
end
|
84
|
+
true
|
78
85
|
end
|
79
86
|
|
80
|
-
protected
|
81
|
-
protected
|
87
|
+
protected "change_#{name}"
|
88
|
+
protected "change_#{name}!"
|
82
89
|
private :_change_state
|
83
90
|
|
84
|
-
define_method
|
85
|
-
|
91
|
+
define_method "can_transition_to_#{name}?" do |new_state|
|
92
|
+
__send__("#{name}_info").can_transition_to?(new_state)
|
86
93
|
end
|
87
94
|
|
88
95
|
# init and configure state info
|
89
|
-
init_state_info(options[:states])
|
90
|
-
|
96
|
+
init_state_info(name, options[:states])
|
97
|
+
__send__("#{name}_infos").values.each do |info|
|
91
98
|
info.expand_to_transitions
|
92
99
|
|
93
|
-
define_method "#{info.name}?" do
|
94
|
-
|
100
|
+
define_method "#{options[:prefix]}#{info.name}?" do
|
101
|
+
current_info = __send__("#{name}_info")
|
102
|
+
!!(current_info && current_info.is?(info.name))
|
95
103
|
end
|
96
104
|
end
|
97
105
|
|
98
|
-
|
99
106
|
define_state_attribute(options)
|
100
107
|
|
101
108
|
# define the event callbacks
|
102
|
-
events = ([
|
109
|
+
events = (["#{name}_change".to_sym] + options[:events])
|
103
110
|
define_callbacks *events
|
104
111
|
|
105
112
|
# define callback helpers
|
@@ -116,21 +123,21 @@ module Stateful
|
|
116
123
|
|
117
124
|
protected
|
118
125
|
def define_state_attribute(options)
|
119
|
-
define_method
|
120
|
-
instance_variable_get(
|
126
|
+
define_method options[:name] do
|
127
|
+
instance_variable_get("@#{options[:name]}") || options[:default]
|
121
128
|
end
|
122
129
|
|
123
|
-
define_method
|
124
|
-
instance_variable_set(
|
130
|
+
define_method "#{options[:name]}=" do |val|
|
131
|
+
instance_variable_set("@#{options[:name]}", val)
|
125
132
|
end
|
126
133
|
end
|
127
134
|
|
128
135
|
private
|
129
136
|
|
130
|
-
def init_state_info(values, parent = nil)
|
131
|
-
values.each do |
|
132
|
-
info =
|
133
|
-
init_state_info(config, info) if info.is_group?
|
137
|
+
def init_state_info(name, values, parent = nil)
|
138
|
+
values.each do |state_name, config|
|
139
|
+
info = __send__("#{name}_infos")[state_name] = Stateful::StateInfo.new(self, name, parent, state_name, config)
|
140
|
+
init_state_info(name, config, info) if info.is_group?
|
134
141
|
end
|
135
142
|
end
|
136
143
|
end
|
data/spec/mongoid_spec.rb
CHANGED
@@ -17,29 +17,71 @@ class Kata
|
|
17
17
|
:approved => :retired,
|
18
18
|
:retired => nil
|
19
19
|
}
|
20
|
+
|
21
|
+
stateful :merge_status, default: :na, events: [:merge, :approve_merge, :reject_merge], states: {
|
22
|
+
na: :pending,
|
23
|
+
pending: [:approved, :rejected],
|
24
|
+
approved: nil,
|
25
|
+
rejected: :pending
|
26
|
+
}
|
27
|
+
|
28
|
+
def publish
|
29
|
+
change_state(:needs_testing, :publish)
|
30
|
+
end
|
31
|
+
|
32
|
+
def persist_state
|
33
|
+
|
34
|
+
end
|
20
35
|
end
|
21
36
|
|
22
|
-
describe Stateful::
|
37
|
+
describe Stateful::MongoidIntegration do
|
23
38
|
let(:kata) {Kata.new}
|
24
39
|
|
25
40
|
it 'should support creating a state field' do
|
26
41
|
Kata.fields.keys.include?('state').should be_true
|
27
42
|
end
|
28
43
|
|
44
|
+
it 'should support callbacks' do
|
45
|
+
kata.publish
|
46
|
+
end
|
47
|
+
|
29
48
|
it 'should support validating state values' do
|
30
49
|
kata.state.should == :draft
|
50
|
+
kata.merge_status.should == :na
|
31
51
|
kata.valid?.should be_true
|
32
52
|
kata.state = :invalid
|
33
53
|
kata.valid?.should be_false
|
34
54
|
end
|
35
55
|
|
56
|
+
it 'should allow states to be set manually' do
|
57
|
+
kata.state = :approved
|
58
|
+
kata.valid?.should be_true
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should support state boolean helpers' do
|
62
|
+
kata.draft?.should be_true
|
63
|
+
kata.beta?.should be_false
|
64
|
+
kata.state = :needs_testing
|
65
|
+
kata.beta?.should be_true
|
66
|
+
end
|
67
|
+
|
36
68
|
it 'should support can_transition_to?' do
|
37
|
-
kata.
|
38
|
-
kata.
|
69
|
+
kata.can_transition_to_state?(:needs_testing).should be_true
|
70
|
+
kata.can_transition_to_state?(:retired).should be_false
|
39
71
|
end
|
40
72
|
|
41
73
|
it 'should create scopes for each state and virtual state' do
|
42
74
|
Kata.beta.selector.should == {"state" => {"$in" => [:needs_testing, :needs_approval]}}
|
43
75
|
Kata.draft.selector.should == {"state" => :draft}
|
44
76
|
end
|
77
|
+
|
78
|
+
it 'should create prefixed scopes for each state and virtual state of custom state fields' do
|
79
|
+
Kata.merge_status_pending.selector.should == {"merge_status" => :pending}
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should support previous_state' do
|
83
|
+
kata.previous_state.should be_nil
|
84
|
+
|
85
|
+
# cant test after creation right now until mongoid is configured correctly
|
86
|
+
end
|
45
87
|
end
|
data/spec/stateful_spec.rb
CHANGED
@@ -25,6 +25,12 @@ class Kata
|
|
25
25
|
:retired => nil
|
26
26
|
}
|
27
27
|
|
28
|
+
stateful :merge_status, default: :na, events: [:merge, :approve_merge, :reject_merge], states: {
|
29
|
+
na: :pending,
|
30
|
+
pending: [:approved, :rejected],
|
31
|
+
approved: nil,
|
32
|
+
rejected: :pending
|
33
|
+
}
|
28
34
|
|
29
35
|
after_state_change do |doc|
|
30
36
|
doc.state_changes += 1
|
@@ -73,25 +79,40 @@ describe Kata do
|
|
73
79
|
|
74
80
|
it 'should support state_infos' do
|
75
81
|
Kata.state_infos.should_not be_nil
|
82
|
+
Kata.merge_status_infos.should_not be_nil
|
76
83
|
end
|
77
84
|
|
78
85
|
it 'should support default state' do
|
79
86
|
kata.state.should == :draft
|
87
|
+
kata.merge_status.should == :na
|
80
88
|
end
|
81
89
|
|
82
90
|
it 'should support state_info' do
|
83
91
|
kata.state_info.should_not be_nil
|
84
92
|
kata.state_info.name.should == :draft
|
93
|
+
|
94
|
+
# custom names
|
95
|
+
kata.merge_status_info.should_not be_nil
|
96
|
+
kata.merge_status_info.name.should == :na
|
85
97
|
end
|
86
98
|
|
87
99
|
it 'should support simple boolean helper methods' do
|
88
100
|
kata.draft?.should be_true
|
89
101
|
kata.published?.should be_false
|
102
|
+
kata.state = :needs_feedback
|
103
|
+
kata.published?.should be_true
|
104
|
+
|
105
|
+
# custom state names
|
106
|
+
kata.merge_status_na?.should be_true
|
107
|
+
kata.merge_status_approved?.should be_false
|
108
|
+
kata.merge_status = :approved
|
109
|
+
kata.merge_status_approved?.should be_true
|
90
110
|
end
|
91
111
|
|
92
112
|
context 'change_state' do
|
93
113
|
it 'should raise error when an invalid transition state is provided' do
|
94
114
|
expect{kata.send(:change_state!, :retired)}.to raise_error
|
115
|
+
expect{kata.send(:change_merge_status!, :approved)}.to raise_error
|
95
116
|
end
|
96
117
|
|
97
118
|
it 'should raise error when a group state is provided' do
|
@@ -104,6 +125,7 @@ describe Kata do
|
|
104
125
|
|
105
126
|
it 'should support state_valid?' do
|
106
127
|
kata.state_valid?.should be_true
|
128
|
+
kata.merge_status_valid?.should be_true
|
107
129
|
end
|
108
130
|
|
109
131
|
it 'should change the state when a proper state is provided' do
|
@@ -116,6 +138,11 @@ describe Kata do
|
|
116
138
|
kata.send(:change_state, :needs_approval).should be_true
|
117
139
|
kata.send(:change_state, :approved).should be_true
|
118
140
|
kata.state.should == :approved
|
141
|
+
|
142
|
+
# custom
|
143
|
+
kata.send(:change_merge_status, :approved).should be_false
|
144
|
+
kata.send(:change_merge_status, :pending).should be_true
|
145
|
+
kata.merge_status.should == :pending
|
119
146
|
end
|
120
147
|
|
121
148
|
it 'should support calling passed blocks when state is valid' do
|
@@ -134,6 +161,15 @@ describe Kata do
|
|
134
161
|
kata.publish
|
135
162
|
kata.state_changes.should == 1
|
136
163
|
end
|
164
|
+
|
165
|
+
it 'should support can_transition_to_state?' do
|
166
|
+
kata.can_transition_to_state?(:needs_feedback).should be_true
|
167
|
+
kata.can_transition_to_state?(:approved).should be_false
|
168
|
+
|
169
|
+
# custom states
|
170
|
+
kata.can_transition_to_merge_status?(:pending).should be_true
|
171
|
+
kata.can_transition_to_merge_status?(:approved).should be_false
|
172
|
+
end
|
137
173
|
end
|
138
174
|
|
139
175
|
describe Stateful::StateInfo do
|
@@ -144,6 +180,9 @@ describe Kata do
|
|
144
180
|
Kata.state_infos[:approved].is?(:published).should be_true
|
145
181
|
Kata.state_infos[:approved].is?(:beta).should be_false
|
146
182
|
Kata.state_infos[:retired].is?(:beta).should be_false
|
183
|
+
|
184
|
+
# custom
|
185
|
+
Kata.merge_status_infos[:na].is?(:na).should be_true
|
147
186
|
end
|
148
187
|
|
149
188
|
it 'should support expanded to transitions' do
|
@@ -152,5 +191,12 @@ describe Kata do
|
|
152
191
|
|
153
192
|
Kata.state_infos[:retired].to_transitions.should be_empty
|
154
193
|
end
|
194
|
+
|
195
|
+
it 'should support can_transition_to?' do
|
196
|
+
Kata.state_infos[:draft].can_transition_to?(:needs_feedback).should be_true
|
197
|
+
Kata.state_infos[:draft].can_transition_to?(:approved).should be_false
|
198
|
+
|
199
|
+
Kata.merge_status_infos[:na].can_transition_to?(:pending).should be_true
|
200
|
+
end
|
155
201
|
end
|
156
202
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stateful
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- jake hoffner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-12-
|
11
|
+
date: 2013-12-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|