stateful 0.0.2 → 0.0.4
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.
- 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
|