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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9261a6a19bbd004baeacff1e9d6b3d33e3096a8c
4
- data.tar.gz: 7c4edb733fb8d48880f3cc89538b200c84a69d8c
3
+ metadata.gz: 43a52ed260948a8b25ab8e179b6cc1a4279243b8
4
+ data.tar.gz: 0099bcace08e9525183ff80bb81b4f8b3b32d6ee
5
5
  SHA512:
6
- metadata.gz: 1a2144835b0991988cc9935bdf70891662552774cda5f9f3d124e4ef6887aabee2d542dbee377f871fb76a500241b546e773b62b1a112b74f47da46623f03130
7
- data.tar.gz: 13aaa3bcbe79a07d9d8ceae4eaf58c4e789351ce981e9c9fe639d119489deffa20fd906fdaa137334545930014351d2cd0d97d2e365090342976e10a010f02a2
6
+ metadata.gz: 01255d072732c11031920c57d6fbadc3c9242329ae4cb7ab4d07239e0a7cb2cebefa34cb7eedd7d4c73ed109764a14f64eb98300acbc34447a818383dce33009
7
+ data.tar.gz: ac27cc34184bbc0cd40676fd2cf1c198b61630080f2d6bd472bd2b30cdf7cdbee4dfe651df688e033d96cbf1085c3dd8326ef85107ab154bcc1ac936d3fea273
@@ -1,26 +1,39 @@
1
1
  module Stateful
2
- module Mongoid
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 :state, type: Symbol, default: options[:default]
10
- validates_inclusion_of :state,
11
- in: state_infos.keys,
12
- message: options.has_key?(:message) ? options[:message] : 'invalid state value',
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
- state_infos.values.each do |info|
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 info.name, where(state: states.first)
20
+ scope scope_name, where(options[:name] => states.first)
20
21
  else
21
- scope info.name, where(:state.in => states)
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
@@ -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 = @state_class.state_infos[state]
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 = @state_class.state_infos[to]
51
+ info = infos[to]
47
52
 
48
53
  if info.is_group?
49
54
  info.collect_child_states
@@ -1,3 +1,3 @@
1
1
  module Stateful
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.4"
3
3
  end
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::Mongoid if included_modules.include?(::Mongoid::Document)
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 'state_events' do
33
+ define_method "#{name}_events" do
25
34
  options[:events]
26
35
  end
27
36
 
28
- define_method 'state_info' do
29
- self.class.state_infos[self.state]
37
+ define_method "#{name}_info" do
38
+ self.class.__send__("#{name}_infos")[__send__(name)]
30
39
  end
31
40
 
32
- define_method 'state_valid?' do
33
- self.class.state_infos.keys.include?(state)
41
+ define_method "#{name}_valid?" do
42
+ self.class.__send__("#{name}_infos").keys.include?(__send__(name))
34
43
  end
35
44
 
36
- define_method 'change_state' do |new_state, options = {}, &block|
37
- return false if new_state == state
38
- return false unless state_info.can_transition_to?(new_state)
39
- _change_state(new_state, options, [:persist_state, :save], &block)
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 'change_state!' do |new_state, options = {}, &block|
43
- raise "transition from #{state} to #{new_state} not allowed" unless state_info.can_transition_to?(new_state)
44
- _change_state(new_state, options, [:persist_state!, :save!], &block)
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 '_change_state' do |new_state, options, persist_methods, &block|
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
- if block and block.call == false
59
- false
60
- else
61
- callbacks = [:state_change]
62
- callbacks << options[:event] if options[:event]
63
- run_callbacks *callbacks do
64
- self.state = new_state
65
-
66
- ## if a specific persist method value was provided
67
- #if options.has_key?(:persist_method)
68
- # # call the method if one was provided
69
- # __send__(options[:persist_method]) if options[:persist_method]
70
- ## if no persist method option was provided than use the defaults
71
- #else
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 :change_state
81
- protected :change_state!
87
+ protected "change_#{name}"
88
+ protected "change_#{name}!"
82
89
  private :_change_state
83
90
 
84
- define_method 'can_transition_to?' do |new_state|
85
- state_info.can_transition_to?(new_state)
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
- state_infos.values.each do |info|
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
- info.is?(self.state)
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 = ([:state_change] + options[: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 'state' do
120
- instance_variable_get(:@state) || options[:default]
126
+ define_method options[:name] do
127
+ instance_variable_get("@#{options[:name]}") || options[:default]
121
128
  end
122
129
 
123
- define_method 'state=' do |val|
124
- instance_variable_set(:@state, val)
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 |name, config|
132
- info = state_infos[name] = Stateful::StateInfo.new(self, parent, name, config)
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::Mongoid do
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.can_transition_to?(:needs_testing).should be_true
38
- kata.can_transition_to?(:retired).should be_false
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
@@ -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.2
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-18 00:00:00.000000000 Z
11
+ date: 2013-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport