stator 0.2.2 → 0.4.0

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