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