stator 0.2.2 → 0.4.0

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
- SHA1:
3
- metadata.gz: 79f3afdff7482b4d1cb783a7bdee4941e78bb3c8
4
- data.tar.gz: 1015c2937be4daef19f9c270f8f5e20a47539232
2
+ SHA256:
3
+ metadata.gz: bd6df35f26ca3ad5d575a7ca144abf6bd17ab7eb24b5ada82fb46e499998ce99
4
+ data.tar.gz: 5e67fc30ac3a46248afee90f68781afb1628a8145ec6da935573a1ee4e29d573
5
5
  SHA512:
6
- metadata.gz: 6f1ebc9d9eecf8af87e94d778dd4eb23f146d772d91d9f4e09739c0927225e2ce15a0026fe6b05d425bbb5ccd3babf0b38604279f787951bb312fcd216d181e2
7
- data.tar.gz: 055f13839c226f3542bd72da2a8683c9a03ee540cfb31206d6ec3b0b4357f698ba8407d1b76418393767f59efd945abc4dc272895b54b6452405f4a97620b304
6
+ metadata.gz: d1d45f5f4797c75bc8b124041cab91b0e85bec9e936ec76fdfc15b53a5f3c88db2cd733546f20f50fdf2ec3edb7f5b3cffeb93996053afc0a2784de6fd9d17e5
7
+ data.tar.gz: 779f3a8c8b04b04acb5abdf84722a7418a0c5a8c35f859d02d8c35419ac6398bd5b64b9a80cadf329f4ae9fde4f666664da2075d500187ea1ba144557c240f47
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.2.3
1
+ 2.5.1
data/.travis.yml CHANGED
@@ -4,12 +4,38 @@ rvm:
4
4
  - 2.0.0
5
5
  - 2.1.6
6
6
  - 2.2.3
7
+ - 2.4.5
8
+ - 2.5.3
7
9
 
8
10
  gemfile:
9
11
  - gemfiles/ar40.gemfile
10
12
  - gemfiles/ar41.gemfile
11
13
  - gemfiles/ar42.gemfile
14
+ - gemfiles/ar52.gemfile
12
15
 
13
16
  matrix:
14
17
  allow_failures:
15
18
  - gemfile: gemfiles/ar42.gemfile
19
+ - gemfile: gemfiles/ar52.gemfile
20
+
21
+ exclude:
22
+ - rvm: 2.0.0
23
+ gemfile: gemfiles/ar52.gemfile
24
+
25
+ - rvm: 2.1.6
26
+ gemfile: gemfiles/ar52.gemfile
27
+
28
+ - rvm: 2.2.3
29
+ gemfile: gemfiles/ar52.gemfile
30
+
31
+ - rvm: 2.4.5
32
+ gemfile: gemfiles/ar40.gemfile
33
+
34
+ - rvm: 2.4.5
35
+ gemfile: gemfiles/ar41.gemfile
36
+
37
+ - rvm: 2.5.3
38
+ gemfile: gemfiles/ar40.gemfile
39
+
40
+ - rvm: 2.5.3
41
+ gemfile: gemfiles/ar41.gemfile
data/Gemfile CHANGED
@@ -1,10 +1,10 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in stator.gemspec
4
- gem 'activerecord', '4.0.0'
4
+ gem 'activerecord', '5.2.3'
5
5
 
6
6
  gemspec
7
7
 
8
+ gem 'activerecord-nulldb-adapter', '~> 0.4.0', require: false
8
9
  gem 'rake'
9
- gem 'activerecord-nulldb-adapter', :require => false, :git => 'git@github.com:nulldb/nulldb.git', :ref => 'ffc7dae4697c6b9fb15bed9edca3acb1f00eb5f0'
10
10
  gem 'rspec'
@@ -1,7 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in stator.gemspec
4
- gem 'activerecord', '~> 4.2.0'
4
+ gem 'activerecord', '~> 4.0.0'
5
5
 
6
6
  gemspec :path => '../'
7
7
 
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in stator.gemspec
4
+ gem 'activerecord', '~> 5.2.0'
5
+
6
+ gemspec :path => '../'
7
+
8
+ gem 'rake'
9
+ gem 'activerecord-nulldb-adapter', '~> 0.4.0', :require => false, :git => 'git@github.com:nulldb/nulldb.git'
10
+ gem 'rspec'
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stator
2
4
  class Integration
3
5
 
4
- delegate :states, :to => :@machine
5
- delegate :transitions, :to => :@machine
6
- delegate :namespace, :to => :@machine
6
+ delegate :states, to: :@machine
7
+ delegate :transitions, to: :@machine
8
+ delegate :namespace, to: :@machine
9
+
10
+ attr_reader :skip_validations
11
+ attr_reader :skip_transition_tracking
7
12
 
8
13
  def initialize(machine, record)
9
14
  @machine = machine
@@ -11,7 +16,7 @@ module Stator
11
16
  end
12
17
 
13
18
  def state=(new_value)
14
- @record.send("#{@machine.field}=", new_value)
19
+ @record.send("#{@machine.field}=", new_value)
15
20
  end
16
21
 
17
22
  def state
@@ -26,6 +31,14 @@ module Stator
26
31
  end
27
32
  end
28
33
 
34
+ def state_by?(state, time)
35
+ field_name = "#{state}_#{@machine.field}_at"
36
+ return false unless @record.respond_to?(field_name)
37
+ return false if @record.send(field_name).nil?
38
+ return true if time.nil?
39
+ @record.send(field_name) <= time
40
+ end
41
+
29
42
  def state_changed?(use_previous = false)
30
43
  if use_previous
31
44
  !!@record.previous_changes[@machine.field.to_s]
@@ -35,11 +48,11 @@ module Stator
35
48
  end
36
49
 
37
50
  def validate_transition
38
- return unless self.state_changed?
39
- return if @machine.skip_validations
51
+ return unless state_changed?
52
+ return if skip_validations
40
53
 
41
- was = self.state_was
42
- is = self.state
54
+ was = state_was
55
+ is = state
43
56
 
44
57
  if @record.new_record?
45
58
  invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
@@ -48,7 +61,7 @@ module Stator
48
61
  end
49
62
  end
50
63
 
51
- # todo: i18n
64
+ # TODO: i18n
52
65
  def invalid_state!
53
66
  @record.errors.add(@machine.field, "is not a valid state")
54
67
  end
@@ -58,10 +71,10 @@ module Stator
58
71
  end
59
72
 
60
73
  def track_transition
61
- return if @machine.skip_transition_tracking
74
+ return if skip_transition_tracking
62
75
 
63
- self.attempt_to_track_state(self.state)
64
- self.attempt_to_track_state_changed_timestamp
76
+ attempt_to_track_state(state)
77
+ attempt_to_track_state_changed_timestamp
65
78
 
66
79
  true
67
80
  end
@@ -82,7 +95,6 @@ module Stator
82
95
 
83
96
  # grab all the states and their timestamps that occur on or after state_at and on or before the time in question
84
97
  later_states = all_states.map do |s|
85
-
86
98
  next if state == s
87
99
 
88
100
  at = @record.send("#{s}_#{@machine.field}_at")
@@ -98,8 +110,8 @@ module Stator
98
110
  return true if later_states.empty?
99
111
 
100
112
  # grab the states that were present at the lowest timestamp
101
- later_groups = later_states.group_by{|s| s[:at] }
102
- later_group_key = later_groups.keys.sort[0]
113
+ later_groups = later_states.group_by { |s| s[:at] }
114
+ later_group_key = later_groups.keys.min
103
115
  later_states = later_groups[later_group_key]
104
116
 
105
117
  # if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
@@ -111,13 +123,30 @@ module Stator
111
123
  end
112
124
 
113
125
  def likely_state_at(t)
114
- @machine.states.reverse.detect{|s| in_state_at?(s,t) }
126
+ @machine.states.reverse.detect { |s| in_state_at?(s, t) }
127
+ end
128
+
129
+ def without_validation
130
+ was = @skip_validations
131
+ @skip_validations = true
132
+ yield @record
133
+ ensure
134
+ @skip_validations = was
135
+ end
136
+
137
+ def without_transition_tracking
138
+ was = @skip_transition_tracking
139
+ @skip_transition_tracking = true
140
+ yield @record
141
+ ensure
142
+ @skip_transition_tracking = was
115
143
  end
116
144
 
117
145
  protected
118
146
 
119
147
  def attempt_to_track_state(state_to_track)
120
148
  return unless state_to_track
149
+
121
150
  _attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
122
151
  end
123
152
 
@@ -128,7 +157,8 @@ module Stator
128
157
  def _attempt_to_track_change(field_name)
129
158
  return unless @record.respond_to?(field_name)
130
159
  return unless @record.respond_to?("#{field_name}=")
131
- return unless @record.send("#{field_name}").nil? || self.state_changed?
160
+ return unless @record.send(field_name.to_s).nil? || state_changed?
161
+ return if @record.send("#{field_name}_changed?")
132
162
 
133
163
  @record.send("#{field_name}=", (Time.zone || Time).now)
134
164
  end
@@ -7,16 +7,14 @@ module Stator
7
7
  attr_reader :transitions
8
8
  attr_reader :states
9
9
  attr_reader :namespace
10
- attr_reader :skip_validations
11
- attr_reader :skip_transition_tracking
12
-
13
10
 
14
11
  def initialize(klass, options = {})
15
- @class_name = klass.name
16
- @field = options[:field] || :state
17
- @namespace = options[:namespace] || nil
12
+ @class_name = klass.name
13
+ @field = options[:field] || :state
14
+ @namespace = options[:namespace]
18
15
 
19
- @initial_state = options[:initial] && options[:initial].to_s
16
+ @initial_state = options[:initial] && options[:initial].to_s
17
+ @tracking_enabled = options[:track] || false
20
18
 
21
19
  @transitions = []
22
20
  @aliases = []
@@ -26,7 +24,6 @@ module Stator
26
24
  @states = [@initial_state].compact
27
25
 
28
26
  @options = options
29
-
30
27
  end
31
28
 
32
29
  def integration(record)
@@ -38,7 +35,6 @@ module Stator
38
35
  end
39
36
 
40
37
  def transition(name, &block)
41
-
42
38
  t = ::Stator::Transition.new(@class_name, name, @namespace)
43
39
  t.instance_eval(&block) if block_given?
44
40
 
@@ -66,8 +62,14 @@ module Stator
66
62
  end
67
63
  end
68
64
 
65
+ def tracking_enabled?
66
+ @tracking_enabled
67
+ end
68
+
69
69
  def conditional(*states, &block)
70
- klass.instance_exec("#{states.map(&:to_s).inspect}.include?(self._stator(#{@namespace.inspect}).integration(self).state)", &block)
70
+ _namespace = @namespace
71
+
72
+ klass.instance_exec(proc { states.map(&:to_s).include?(self._stator(_namespace).integration(self).state) }, &block)
71
73
  end
72
74
 
73
75
  def matching_transition(from, to)
@@ -86,22 +88,6 @@ module Stator
86
88
  @class_name.constantize
87
89
  end
88
90
 
89
- def without_validation
90
- was = @skip_validations
91
- @skip_validations = true
92
- yield
93
- ensure
94
- @skip_validations = was
95
- end
96
-
97
- def without_transition_tracking
98
- was = @skip_transition_tracking
99
- @skip_transition_tracking = true
100
- yield
101
- ensure
102
- @skip_transition_tracking = was
103
- end
104
-
105
91
  protected
106
92
 
107
93
  def verify_transition_validity(transition)
@@ -131,6 +117,11 @@ module Stator
131
117
  integration = self._stator(#{@namespace.inspect}).integration(self)
132
118
  integration.state == #{state.to_s.inspect}
133
119
  end
120
+
121
+ def #{method_name}_state_by?(time)
122
+ integration = self._stator(#{@namespace.inspect}).integration(self)
123
+ integration.state_by?(#{state.to_s.inspect}, time)
124
+ end
134
125
  EV
135
126
  end
136
127
  end
data/lib/stator/model.rb CHANGED
@@ -35,26 +35,29 @@ module Stator
35
35
 
36
36
  def self.included(base)
37
37
  base.class_eval do
38
- before_save :_stator_track_transition, prepend: true
38
+ before_save :_stator_maybe_track_transition, prepend: true
39
39
  end
40
40
  end
41
41
 
42
42
  def in_state_at?(state, t, namespace = '')
43
- machine = self._stator(namespace)
44
- machine.integration(self).in_state_at?(state, t)
43
+ _integration(namespace).in_state_at?(state, t)
45
44
  end
46
45
 
47
46
  def likely_state_at(t, namespace = '')
48
- machine = self._stator(namespace)
49
- machine.integration(self).likely_state_at(t)
47
+ _integration(namespace).likely_state_at(t)
50
48
  end
51
49
 
52
- protected
50
+ def state_by?(state, t, namespace = '')
51
+ _integration(namespace).state_by?(state, t)
52
+ end
53
53
 
54
+ protected
54
55
 
55
- def _stator_track_transition
56
+ def _stator_maybe_track_transition
56
57
  self._stators.each do |namespace, machine|
57
- machine.integration(self).track_transition
58
+ next unless machine.tracking_enabled?
59
+
60
+ _integration(namespace).track_transition
58
61
  end
59
62
 
60
63
  true
@@ -70,23 +73,28 @@ module Stator
70
73
  end
71
74
  end
72
75
 
76
+ def initialize_dup(other)
77
+ @_integrations = {}
78
+ super
79
+ end
80
+
73
81
  def without_state_transition_validations(namespace = '')
74
- self._stator(namespace).without_validation do
75
- yield
82
+ _integration(namespace).without_validation do
83
+ yield self
76
84
  end
77
85
  end
78
86
 
79
87
  def without_state_transition_tracking(namespace = '')
80
- self._stator(namespace).without_transition_tracking do
81
- yield
88
+ _integration(namespace).without_transition_tracking do
89
+ yield self
82
90
  end
83
91
  end
84
92
 
85
93
  protected
86
94
 
87
95
  def _stator_validate_transition
88
- self._stators.each do |namespace, machine|
89
- machine.integration(self).validate_transition
96
+ self._stators.each_key do |namespace|
97
+ _integration(namespace).validate_transition
90
98
  end
91
99
  end
92
100
 
@@ -94,6 +102,12 @@ module Stator
94
102
  self.class._stator(namespace)
95
103
  end
96
104
 
105
+ def _integration(namespace = '')
106
+ @_integrations ||= {}
107
+ @_integrations[namespace] ||= _stator(namespace).integration(self)
108
+ @_integrations[namespace]
109
+ end
110
+
97
111
  end
98
112
  end
99
113
  end
@@ -42,7 +42,7 @@ module Stator
42
42
  end
43
43
 
44
44
  def conditional(options = {}, &block)
45
- klass.instance_exec(conditional_string(options), &block)
45
+ klass.instance_exec(conditional_block(options), &block)
46
46
  end
47
47
 
48
48
  def any
@@ -63,25 +63,30 @@ module Stator
63
63
  @callbacks[kind] || []
64
64
  end
65
65
 
66
- def conditional_string(options = {})
66
+ def conditional_block(options = {})
67
67
  options[:use_previous] ||= false
68
- %Q{
69
- (
70
- self._stator(#{@namespace.inspect}).integration(self).state_changed?(#{options[:use_previous].inspect})
71
- ) && (
72
- #{@froms.inspect}.include?(self._stator(#{@namespace.inspect}).integration(self).state_was(#{options[:use_previous].inspect})) ||
73
- #{@froms.inspect}.include?(::Stator::Transition::ANY)
74
- ) && (
75
- self._stator(#{@namespace.inspect}).integration(self).state == #{@to.inspect} ||
76
- #{@to.inspect} == ::Stator::Transition::ANY
77
- )
78
- }
68
+
69
+ _namespace = @namespace
70
+ _froms = @froms
71
+ _to = @to
72
+
73
+ Proc.new do
74
+ (
75
+ self._stator(_namespace).integration(self).state_changed?(options[:use_previous])
76
+ ) && (
77
+ _froms.include?(self._stator(_namespace).integration(self).state_was(options[:use_previous])) ||
78
+ _froms.include?(::Stator::Transition::ANY)
79
+ ) && (
80
+ self._stator(_namespace).integration(self).state == _to ||
81
+ _to == ::Stator::Transition::ANY
82
+ )
83
+ end
79
84
  end
80
85
 
81
86
  def generate_methods
82
87
  klass.class_eval <<-EV, __FILE__, __LINE__ + 1
83
88
  def #{@full_name}(should_save = true)
84
- integration = self._stator(#{@namespace.inspect}).integration(self)
89
+ integration = _integration(#{@namespace.to_s.inspect})
85
90
 
86
91
  unless can_#{@full_name}?
87
92
  integration.invalid_transition!(integration.state, #{@to.inspect}) if should_save
@@ -93,7 +98,7 @@ module Stator
93
98
  end
94
99
 
95
100
  def #{@full_name}!
96
- integration = self._stator(#{@namespace.inspect}).integration(self)
101
+ integration = _integration(#{@namespace.to_s.inspect})
97
102
 
98
103
  unless can_#{@full_name}?
99
104
  integration.invalid_transition!(integration.state, #{@to.inspect})
@@ -105,10 +110,10 @@ module Stator
105
110
  end
106
111
 
107
112
  def can_#{@full_name}?
108
- machine = self._stator(#{@namespace.inspect})
109
- return true if machine.skip_validations
113
+ integration = _integration(#{@namespace.to_s.inspect})
114
+ return true if integration.skip_validations
110
115
 
111
- integration = machine.integration(self)
116
+ machine = self._stator(#{@namespace.to_s.inspect})
112
117
  transition = machine.transitions.detect{|t| t.full_name.to_s == #{@full_name.inspect}.to_s }
113
118
  transition.can?(integration.state)
114
119
  end
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stator
4
+
2
5
  MAJOR = 0
3
- MINOR = 2
4
- PATCH = 2
6
+ MINOR = 4
7
+ PATCH = 0
5
8
  PRERELEASE = nil
6
9
 
7
- VERSION = [MAJOR, MINOR, PATCH, PRERELEASE].compact.join('.')
10
+ VERSION = [MAJOR, MINOR, PATCH, PRERELEASE].compact.join(".")
11
+
8
12
  end
data/spec/model_spec.rb CHANGED
@@ -1,101 +1,104 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- describe Stator::Model do
3
+ require "spec_helper"
4
4
 
5
- it 'should set the default state after initialization' do
5
+ describe Stator::Model do
6
+ it "should set the default state after initialization" do
6
7
  u = User.new
7
- u.state.should eql('pending')
8
+ u.state.should eql("pending")
8
9
  end
9
10
 
10
- it 'should see the initial setting of the state as a change with the initial state as the previous value' do
11
+ it "should see the initial setting of the state as a change with the initial state as the previous value" do
11
12
  u = User.new
12
- u.state = 'activated'
13
- u.state_was.should eql('pending')
13
+ u.state = "activated"
14
+ u.state_was.should eql("pending")
14
15
  end
15
16
 
16
- it 'should not obstruct normal validations' do
17
+ it "should not obstruct normal validations" do
17
18
  u = User.new
18
19
  u.should_not be_valid
19
20
  u.errors[:email].grep(/length/).should_not be_empty
20
21
  end
21
22
 
22
- it 'should ensure a valid state transition when given a bogus state' do
23
+ it "should ensure a valid state transition when given a bogus state" do
23
24
  u = User.new
24
- u.state = 'anythingelse'
25
+ u.state = "anythingelse"
25
26
 
26
27
  u.should_not be_valid
27
- u.errors[:state].should eql(['is not a valid state'])
28
+ u.errors[:state].should eql(["is not a valid state"])
28
29
  end
29
30
 
30
- it 'should allow creation at any state' do
31
- u = User.new(:email => 'doug@example.com')
32
- u.state = 'hyperactivated'
31
+ it "should allow creation at any state" do
32
+ u = User.new(email: "doug@example.com")
33
+ u.state = "hyperactivated"
33
34
 
34
35
  u.should be_valid
35
36
  end
36
37
 
37
- it 'should ensure a valid state transition when given an illegal state based on the current state' do
38
+ it "should ensure a valid state transition when given an illegal state based on the current state" do
38
39
  u = User.new
39
- u.stub(:new_record?).and_return(false)
40
- u.state = 'hyperactivated'
40
+
41
+ allow(u).to receive(:new_record?).and_return(false)
42
+
43
+ u.state = "hyperactivated"
41
44
 
42
45
  u.should_not be_valid
43
46
  u.errors[:state].should_not be_empty
44
47
  end
45
48
 
46
- it 'should not allow a transition that is currently in a `to` state' do
47
- u = User.new(:email => 'fred@example.com')
49
+ it "should not allow a transition that is currently in a `to` state" do
50
+ u = User.new(email: "fred@example.com")
48
51
  u.activate!
49
52
  u.hyperactivate!
50
53
 
51
- lambda{
54
+ lambda {
52
55
  u.hyperactivate!
53
56
  }.should raise_error(/cannot transition to \"hyperactivated\" from \"hyperactivated\"/)
54
57
  end
55
58
 
56
- it 'should run conditional validations' do
59
+ it "should run conditional validations" do
57
60
  u = User.new
58
- u.state = 'semiactivated'
61
+ u.state = "semiactivated"
59
62
  u.should_not be_valid
60
63
 
61
64
  u.errors[:state].should be_empty
62
65
  u.errors[:email].grep(/format/).should_not be_empty
63
66
  end
64
67
 
65
- it 'should invoke callbacks' do
66
- u = User.new(:activated => true, :email => 'doug@example.com', :name => 'doug')
68
+ it "should invoke callbacks" do
69
+ u = User.new(activated: true, email: "doug@example.com", name: "doug")
67
70
  u.activated.should == true
68
71
 
69
72
  u.deactivate
70
73
 
71
74
  u.activated.should == false
72
- u.state.should eql('deactivated')
75
+ u.state.should eql("deactivated")
73
76
  u.activated_state_at.should be_nil
74
77
  u.should be_persisted
75
78
  end
76
79
 
77
- it 'should blow up if the record is invalid and a bang method is used' do
78
- u = User.new(:email => 'doug@other.com', :name => 'doug')
79
- lambda{
80
+ it "should blow up if the record is invalid and a bang method is used" do
81
+ u = User.new(email: "doug@other.com", name: "doug")
82
+ lambda {
80
83
  u.activate!
81
84
  }.should raise_error(ActiveRecord::RecordInvalid)
82
85
  end
83
86
 
84
- it 'should allow for other fields to be used other than state' do
87
+ it "should allow for other fields to be used other than state" do
85
88
  a = Animal.new
86
89
  a.should be_valid
87
90
 
88
91
  a.birth!
89
92
  end
90
93
 
91
- it 'should create implicit transitions for state declarations' do
94
+ it "should create implicit transitions for state declarations" do
92
95
  a = Animal.new
93
96
  a.should_not be_grown_up
94
- a.status = 'grown_up'
97
+ a.status = "grown_up"
95
98
  a.save
96
99
  end
97
100
 
98
- it 'should allow multiple machines in the same model' do
101
+ it "should allow multiple machines in the same model" do
99
102
  f = Farm.new
100
103
  f.should be_dirty
101
104
  f.should be_house_dirty
@@ -110,30 +113,30 @@ describe Stator::Model do
110
113
  f.should_not be_house_dirty
111
114
  end
112
115
 
113
- it 'should allow saving to be skipped' do
116
+ it "should allow saving to be skipped" do
114
117
  f = Farm.new
115
118
  f.cleanup(false)
116
119
 
117
120
  f.should_not be_persisted
118
121
  end
119
122
 
120
- it 'should allow no initial state' do
123
+ it "should allow no initial state" do
121
124
  f = Factory.new
122
125
  f.state.should be_nil
123
126
 
124
127
  f.construct.should eql(true)
125
128
 
126
- f.state.should eql('constructed')
129
+ f.state.should eql("constructed")
127
130
  end
128
131
 
129
- it 'should allow any transition if validations are opted out of' do
132
+ it "should allow any transition if validations are opted out of" do
130
133
  u = User.new
131
- u.email = 'doug@example.com'
134
+ u.email = "doug@example.com"
132
135
 
133
136
  u.can_hyperactivate?.should eql(false)
134
137
  u.hyperactivate.should eql(false)
135
138
 
136
- u.state.should eql('pending')
139
+ u.state.should eql("pending")
137
140
 
138
141
  u.without_state_transition_validations do
139
142
  u.can_hyperactivate?.should eql(true)
@@ -141,13 +144,13 @@ describe Stator::Model do
141
144
  end
142
145
  end
143
146
 
144
- it 'should skip tracking timestamps if opted out of' do
147
+ it "should skip tracking timestamps if opted out of" do
145
148
  u = User.new
146
- u.email = 'doug@example.com'
149
+ u.email = "doug@example.com"
147
150
 
148
151
  u.without_state_transition_tracking do
149
152
  u.semiactivate!
150
- u.state.should eql('semiactivated')
153
+ u.state.should eql("semiactivated")
151
154
  u.semiactivated_state_at.should be_nil
152
155
  end
153
156
 
@@ -157,9 +160,44 @@ describe Stator::Model do
157
160
  u.activated_state_at.should_not be_nil
158
161
  end
159
162
 
160
- describe 'helper methods' do
163
+ it "should skip tracking timestamps if opted out of with thread safety" do
164
+ threads = []
165
+ skip = User.new(email: "skip@example.com")
166
+ nope = User.new(email: "nope@example.com")
161
167
 
162
- it 'should answer the question of whether the state is currently the one invoked' do
168
+ threads << Thread.new do
169
+ sleep 0.5
170
+ nope.semiactivate!
171
+ end
172
+ threads << Thread.new do
173
+ skip.without_state_transition_tracking do
174
+ sleep 1
175
+ skip.semiactivate!
176
+ end
177
+ end
178
+
179
+ threads.each(&:join)
180
+
181
+ nope.semiactivated_state_at.should_not be_nil
182
+ skip.semiactivated_state_at.should be_nil
183
+ end
184
+
185
+ it "should not inherit _integration cache on dup" do
186
+ u = User.new(email: "user@example.com")
187
+ u.save!
188
+
189
+ u_duped = u.dup
190
+
191
+ u.semiactivate!
192
+
193
+ u_duped_integration = u_duped.send(:_integration)
194
+
195
+ u_duped_integration.state.should_not eql(u.state)
196
+ u_duped_integration.instance_values["record"].should eq(u_duped)
197
+ end
198
+
199
+ describe "helper methods" do
200
+ it "should answer the question of whether the state is currently the one invoked" do
163
201
  a = Animal.new
164
202
  a.should be_unborn
165
203
  a.should_not be_born
@@ -170,7 +208,7 @@ describe Stator::Model do
170
208
  a.should_not be_unborn
171
209
  end
172
210
 
173
- it 'should determine if it can validly execute a transition' do
211
+ it "should determine if it can validly execute a transition" do
174
212
  a = Animal.new
175
213
  a.can_birth?.should eql(true)
176
214
 
@@ -178,22 +216,20 @@ describe Stator::Model do
178
216
 
179
217
  a.can_birth?.should eql(false)
180
218
  end
181
-
182
219
  end
183
220
 
184
- describe 'tracker methods' do
185
-
221
+ describe "tracker methods" do
186
222
  before do
187
- Time.zone = 'Eastern Time (US & Canada)'
223
+ Time.zone = "Eastern Time (US & Canada)"
188
224
  end
189
225
 
190
- it 'should store the initial state timestamp when the record is created' do
226
+ it "should store the initial state timestamp when the record is created" do
191
227
  a = Animal.new
192
228
  a.save
193
229
  a.unborn_status_at.should be_within(1).of(Time.zone.now)
194
230
  end
195
231
 
196
- it 'should store when a record changed state for the first time' do
232
+ it "should store when a record changed state for the first time" do
197
233
  a = Animal.new
198
234
  a.unborn_status_at.should be_nil
199
235
  a.born_status_at.should be_nil
@@ -202,7 +238,7 @@ describe Stator::Model do
202
238
  a.born_status_at.should be_within(1).of(Time.zone.now)
203
239
  end
204
240
 
205
- it 'should store when a record change states' do
241
+ it "should store when a record change states" do
206
242
  a = Animal.new
207
243
  a.status_changed_at.should be_nil
208
244
 
@@ -216,12 +252,11 @@ describe Stator::Model do
216
252
  a.save
217
253
 
218
254
  a.status_changed_at.should eql(previous_status_changed_at)
219
-
220
255
  end
221
256
 
222
- it 'should prepend the setting of the timestamp so other callbacks can use it' do
257
+ it "should prepend the setting of the timestamp so other callbacks can use it" do
223
258
  u = User.new
224
- u.email = 'doug@example.com'
259
+ u.email = "doug@example.com"
225
260
 
226
261
  u.tagged_at.should be_nil
227
262
  u.semiactivate!
@@ -230,11 +265,93 @@ describe Stator::Model do
230
265
  u.tagged_at.should_not be_nil
231
266
  end
232
267
 
268
+ it "should respect the timestamp if explicitly provided" do
269
+ t = Time.at(Time.now.to_i - 3600)
270
+
271
+ u = User.new
272
+ u.email = "doug@example.com"
273
+ u.state = "semiactivated"
274
+ u.semiactivated_state_at = t
275
+ u.save!
276
+
277
+ u.state.should eql("semiactivated")
278
+ u.semiactivated_state_at.should eql(t)
279
+ end
280
+
281
+ it "should respect the timestamp if explicitly provided via create" do
282
+ t = Time.at(Time.now.to_i - 3600)
283
+
284
+ u = User.create!(
285
+ email: "doug@example.com",
286
+ state: "semiactivated",
287
+ semiactivated_state_at: t
288
+ )
289
+
290
+ u.state.should eql("semiactivated")
291
+ u.semiactivated_state_at.should eql(t)
292
+ end
293
+
294
+ it "should allow opting into track by namespace" do
295
+ z = ZooKeeper.new(name: "Doug")
296
+ z.employment_state.should eql("hired")
297
+ z.employment_fire!
298
+ z.fired_employment_state_at.should_not be_nil
299
+
300
+ z.employment_hire!
301
+ z.hired_employment_state_at.should_not be_nil
302
+
303
+ z.working_start!
304
+ z.started_working_state_at.should be_nil
305
+ z.working_end!
306
+ z.ended_working_state_at.should be_nil
307
+ end
308
+
309
+ describe "#state_by?" do
310
+ it "should be true when the transition is earlier" do
311
+ t = Time.now
312
+ u = User.create!( email: "doug@example.com", activated_state_at: t)
313
+ u.state_by?(:activated, Time.at(t.to_i + 1)).should be true
314
+ u.activated_state_by?(Time.at(t.to_i + 1)).should be true
315
+ end
316
+
317
+ it "should be true when the transition is at the same time" do
318
+ t = Time.now
319
+ u = User.create!( email: "doug@example.com", activated_state_at: t)
320
+ u.state_by?(:activated, t).should be true
321
+ u.activated_state_by?(t).should be true
322
+ end
323
+
324
+ it "should be false when the transition is later" do
325
+ t = Time.now
326
+ u = User.create!( email: "doug@example.com", activated_state_at: t)
327
+ u.state_by?(:activated, Time.at(t.to_i - 1)).should be false
328
+ u.activated_state_by?(Time.at(t.to_i - 1)).should be false
329
+ end
330
+
331
+ it "should be false when the transition is nil" do
332
+ t = Time.now
333
+ u = User.create!( email: "doug@example.com", activated_state_at: nil)
334
+ u.state_by?(:activated, t).should be false
335
+ u.activated_state_by?(t).should be false
336
+ end
337
+
338
+ it "should be true when the transition is not nil and the time is nil" do
339
+ u = User.create!( email: "doug@example.com", activated_state_at: Time.now)
340
+ u.state_by?(:activated, nil).should be true
341
+ u.activated_state_by?(nil).should be true
342
+ end
343
+
344
+ it "should be false when both are nil" do
345
+ u = User.create!(email: "doug@example.com", activated_state_at: nil)
346
+ u.state_by?(:activated, nil).should be false
347
+ u.activated_state_by?(nil).should be false
348
+ end
349
+ end
233
350
  end
234
351
 
235
- describe 'aliasing' do
236
- it 'should allow aliasing within the dsl' do
237
- u = User.new(:email => 'doug@example.com')
352
+ describe "aliasing" do
353
+ it "should allow aliasing within the dsl" do
354
+ u = User.new(email: "doug@example.com")
238
355
  u.should respond_to(:active?)
239
356
  u.should respond_to(:inactive?)
240
357
 
@@ -251,11 +368,11 @@ describe Stator::Model do
251
368
  u.should be_active
252
369
  u.should_not be_inactive
253
370
 
254
- User::ACTIVE_STATES.should eql(['activated', 'hyperactivated'])
255
- User::INACTIVE_STATES.should eql(['pending', 'deactivated', 'semiactivated'])
371
+ User::ACTIVE_STATES.should eql(%w[activated hyperactivated])
372
+ User::INACTIVE_STATES.should eql(%w[pending deactivated semiactivated])
256
373
 
257
- User.active.to_sql.gsub(' ', ' ').should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
258
- User.inactive.to_sql.gsub(' ', ' ').should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
374
+ User.active.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
375
+ User.inactive.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
259
376
  end
260
377
 
261
378
  it "should evaluate inverses correctly" do
@@ -270,7 +387,7 @@ describe Stator::Model do
270
387
  f.should be_house_cleaned
271
388
  end
272
389
 
273
- it 'should namespace aliases just like everything else' do
390
+ it "should namespace aliases just like everything else" do
274
391
  f = Farm.new
275
392
  f.should respond_to(:house_cleaned?)
276
393
 
@@ -280,24 +397,23 @@ describe Stator::Model do
280
397
  f.should be_house_cleaned
281
398
  end
282
399
 
283
- it 'should allow for explicit constant and scope names to be provided' do
400
+ it "should allow for explicit constant and scope names to be provided" do
284
401
  User.should respond_to(:luke_warmers)
285
402
  (!!defined?(User::LUKE_WARMERS)).should eql(true)
286
403
  u = User.new
287
404
  u.should respond_to(:luke_warm?)
288
405
  end
289
406
 
290
- it 'should not create constants or scopes by default' do
407
+ it "should not create constants or scopes by default" do
291
408
  u = User.new
292
409
  u.should respond_to(:iced_tea?)
293
410
  (!!defined?(User::ICED_TEA_STATES)).should eql(false)
294
411
  User.should_not respond_to(:iced_tea)
295
412
  end
296
413
 
297
- it 'should determine the full list of states correctly' do
414
+ it "should determine the full list of states correctly" do
298
415
  states = User._stator("").states
299
- states.should eql(["pending", "activated", "deactivated", "semiactivated", "hyperactivated"])
416
+ states.should eql(%w[pending activated deactivated semiactivated hyperactivated])
300
417
  end
301
418
  end
302
-
303
419
  end
data/spec/spec_helper.rb CHANGED
@@ -12,6 +12,7 @@ require 'stator'
12
12
 
13
13
  RSpec.configure do |config|
14
14
  config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.expect_with(:rspec) { |c| c.syntax = :should }
15
16
  config.run_all_when_everything_filtered = true
16
17
  config.filter_run :focus
17
18
 
@@ -134,6 +134,33 @@ class Zoo < ActiveRecord::Base
134
134
  end
135
135
  end
136
136
 
137
+ class ZooKeeper < ActiveRecord::Base
138
+ extend Stator::Model
139
+
140
+ stator namespace: 'employment', field: 'employment_state', track: true do
141
+ transition :hire do
142
+ from nil, :fired
143
+ to :hired
144
+ end
145
+
146
+ transition :fire do
147
+ from :hired
148
+ to :fired
149
+ end
150
+ end
151
+
152
+ stator namespace: 'working', field: 'working_state', track: false do
153
+ transition :start do
154
+ from nil, :ended
155
+ to :started
156
+ end
157
+
158
+ transition :end do
159
+ from :started
160
+ to :ended
161
+ end
162
+ end
163
+ end
137
164
 
138
165
  class Farm < ActiveRecord::Base
139
166
  extend Stator::Model
@@ -22,6 +22,16 @@ ActiveRecord::Schema.define(:version => 20130628161227) do
22
22
  t.datetime "born_status_at"
23
23
  end
24
24
 
25
+ create_table "zoo_keepers", :force => true do |t|
26
+ t.string "name"
27
+ t.string "employment_state", :default => 'hired'
28
+ t.datetime "hired_employment_state_at"
29
+ t.datetime "fired_employment_state_at"
30
+ t.string "working_state"
31
+ t.datetime "started_working_state_at"
32
+ t.datetime "ended_working_state_at"
33
+ end
34
+
25
35
  create_table "zoos", :force => true do |t|
26
36
  t.string "name"
27
37
  t.string "state", :default => 'closed'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Nelson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-06 00:00:00.000000000 Z
11
+ date: 2021-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -44,6 +44,7 @@ files:
44
44
  - gemfiles/ar40.gemfile
45
45
  - gemfiles/ar41.gemfile
46
46
  - gemfiles/ar42.gemfile
47
+ - gemfiles/ar52.gemfile
47
48
  - lib/stator.rb
48
49
  - lib/stator/alias.rb
49
50
  - lib/stator/integration.rb
@@ -74,8 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
75
  - !ruby/object:Gem::Version
75
76
  version: '0'
76
77
  requirements: []
77
- rubyforge_project:
78
- rubygems_version: 2.4.5.1
78
+ rubygems_version: 3.0.6
79
79
  signing_key:
80
80
  specification_version: 4
81
81
  summary: The simplest of ActiveRecord state machines