stator 0.2.0 → 0.3.3

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
  SHA256:
3
- metadata.gz: f80f0783f3bbff0062fee1ebbb2e4f32ebb2f4dc8605809aab25273482a5f09f
4
- data.tar.gz: 8a8f5f91f72f29fae9579048aae6e1e42c105f9fabf29f698b5c2b9c0382279d
3
+ metadata.gz: c4db54f443ba466ae3fc6f79857816b8719e0cbacf0615f8c014ae0c71c32e81
4
+ data.tar.gz: 53d72d27aafa1a340c02d2d5459235488bf5e12df9198312ffe1d5121fc24912
5
5
  SHA512:
6
- metadata.gz: 21c4d54970b04402dba07658b815c13009bd0dd0a1f8c440202f62fa1f6670848a45abc163815c75d0ca69694343a139e76f5491ac83a768f364efd81c4f7500
7
- data.tar.gz: '0280f986830d8065267696f1bc264123d75b06688d6327657e83ea7d49db7f95ffdf4f7eca0b1d8e3677cb3758d4e4d3eaee8b3bbba21c96c7e7725f949858ee'
6
+ metadata.gz: a2e76eb8e8bbe0e1dab6d8fbdbb6516f12cff56c26c3d10d974d4aadce08fbcabdcd49dbba710d0f904a1a04ea4c489125b18475a119de5e625a9cbb6e9406a4
7
+ data.tar.gz: 0fef985f07d95f3d8801f4e3534f478feb516fea223532e1e562cc85bebdc54c09222ee0087517d4c3f396f4d094ad80a9404f7e0cb69829ffa6c5ddc40bb297
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,27 +1,28 @@
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
10
15
  @record = record
11
16
  end
12
17
 
13
-
14
-
15
-
16
18
  def state=(new_value)
17
- @record.send("#{@machine.field}=", new_value)
19
+ @record.send("#{@machine.field}=", new_value)
18
20
  end
19
21
 
20
22
  def state
21
23
  @record.send(@machine.field)
22
24
  end
23
25
 
24
-
25
26
  def state_was(use_previous = false)
26
27
  if use_previous
27
28
  @record.previous_changes[@machine.field.to_s].try(:[], 0)
@@ -30,7 +31,6 @@ module Stator
30
31
  end
31
32
  end
32
33
 
33
-
34
34
  def state_changed?(use_previous = false)
35
35
  if use_previous
36
36
  !!@record.previous_changes[@machine.field.to_s]
@@ -39,14 +39,12 @@ module Stator
39
39
  end
40
40
  end
41
41
 
42
-
43
-
44
42
  def validate_transition
45
- return unless self.state_changed?
46
- return if @machine.skip_validations
43
+ return unless state_changed?
44
+ return if skip_validations
47
45
 
48
- was = self.state_was
49
- is = self.state
46
+ was = state_was
47
+ is = state
50
48
 
51
49
  if @record.new_record?
52
50
  invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
@@ -55,7 +53,7 @@ module Stator
55
53
  end
56
54
  end
57
55
 
58
- # todo: i18n
56
+ # TODO: i18n
59
57
  def invalid_state!
60
58
  @record.errors.add(@machine.field, "is not a valid state")
61
59
  end
@@ -65,10 +63,10 @@ module Stator
65
63
  end
66
64
 
67
65
  def track_transition
68
- return if @machine.skip_transition_tracking
66
+ return if skip_transition_tracking
69
67
 
70
- self.attempt_to_track_state(self.state)
71
- self.attempt_to_track_state_changed_timestamp
68
+ attempt_to_track_state(state)
69
+ attempt_to_track_state_changed_timestamp
72
70
 
73
71
  true
74
72
  end
@@ -89,7 +87,6 @@ module Stator
89
87
 
90
88
  # grab all the states and their timestamps that occur on or after state_at and on or before the time in question
91
89
  later_states = all_states.map do |s|
92
-
93
90
  next if state == s
94
91
 
95
92
  at = @record.send("#{s}_#{@machine.field}_at")
@@ -105,8 +102,8 @@ module Stator
105
102
  return true if later_states.empty?
106
103
 
107
104
  # grab the states that were present at the lowest timestamp
108
- later_groups = later_states.group_by{|s| s[:at] }
109
- later_group_key = later_groups.keys.sort[0]
105
+ later_groups = later_states.group_by { |s| s[:at] }
106
+ later_group_key = later_groups.keys.min
110
107
  later_states = later_groups[later_group_key]
111
108
 
112
109
  # if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
@@ -118,14 +115,30 @@ module Stator
118
115
  end
119
116
 
120
117
  def likely_state_at(t)
121
- @machine.states.reverse.detect{|s| in_state_at?(s,t) }
118
+ @machine.states.reverse.detect { |s| in_state_at?(s, t) }
122
119
  end
123
120
 
121
+ def without_validation
122
+ was = @skip_validations
123
+ @skip_validations = true
124
+ yield @record
125
+ ensure
126
+ @skip_validations = was
127
+ end
128
+
129
+ def without_transition_tracking
130
+ was = @skip_transition_tracking
131
+ @skip_transition_tracking = true
132
+ yield @record
133
+ ensure
134
+ @skip_transition_tracking = was
135
+ end
124
136
 
125
137
  protected
126
138
 
127
139
  def attempt_to_track_state(state_to_track)
128
140
  return unless state_to_track
141
+
129
142
  _attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
130
143
  end
131
144
 
@@ -136,7 +149,8 @@ module Stator
136
149
  def _attempt_to_track_change(field_name)
137
150
  return unless @record.respond_to?(field_name)
138
151
  return unless @record.respond_to?("#{field_name}=")
139
- return unless @record.send("#{field_name}").nil? || self.state_changed?
152
+ return unless @record.send(field_name.to_s).nil? || state_changed?
153
+ return if @record.send("#{field_name}_changed?")
140
154
 
141
155
  @record.send("#{field_name}=", (Time.zone || Time).now)
142
156
  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]
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)
data/lib/stator/model.rb CHANGED
@@ -14,7 +14,7 @@ module Stator
14
14
  f = options[:field] || :state
15
15
  # rescue nil since the table may not exist yet.
16
16
  initial = self.columns_hash[f.to_s].default rescue nil
17
- options = options.reverse_merge(initial: initial)
17
+ options = options.merge(initial: initial) if initial
18
18
  end
19
19
 
20
20
  machine = (self._stators[options[:namespace].to_s] ||= ::Stator::Machine.new(self, options))
@@ -35,27 +35,25 @@ module Stator
35
35
 
36
36
  def self.included(base)
37
37
  base.class_eval do
38
- before_validation :_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
50
  protected
53
51
 
54
-
55
- def _stator_track_transition
56
-
52
+ def _stator_maybe_track_transition
57
53
  self._stators.each do |namespace, machine|
58
- machine.integration(self).track_transition
54
+ next unless machine.tracking_enabled?
55
+
56
+ _integration(namespace).track_transition
59
57
  end
60
58
 
61
59
  true
@@ -71,23 +69,28 @@ module Stator
71
69
  end
72
70
  end
73
71
 
72
+ def initialize_dup(other)
73
+ @_integrations = {}
74
+ super
75
+ end
76
+
74
77
  def without_state_transition_validations(namespace = '')
75
- self._stator(namespace).without_validation do
76
- yield
78
+ _integration(namespace).without_validation do
79
+ yield self
77
80
  end
78
81
  end
79
82
 
80
83
  def without_state_transition_tracking(namespace = '')
81
- self._stator(namespace).without_transition_tracking do
82
- yield
84
+ _integration(namespace).without_transition_tracking do
85
+ yield self
83
86
  end
84
87
  end
85
88
 
86
89
  protected
87
90
 
88
91
  def _stator_validate_transition
89
- self._stators.each do |namespace, machine|
90
- machine.integration(self).validate_transition
92
+ self._stators.each_key do |namespace|
93
+ _integration(namespace).validate_transition
91
94
  end
92
95
  end
93
96
 
@@ -95,6 +98,12 @@ module Stator
95
98
  self.class._stator(namespace)
96
99
  end
97
100
 
101
+ def _integration(namespace = '')
102
+ @_integrations ||= {}
103
+ @_integrations[namespace] ||= _stator(namespace).integration(self)
104
+ @_integrations[namespace]
105
+ end
106
+
98
107
  end
99
108
  end
100
109
  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 = 0
6
+ MINOR = 3
7
+ PATCH = 3
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")
167
+
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)
161
194
 
162
- it 'should answer the question of whether the state is currently the one invoked' do
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,23 +252,64 @@ 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
259
+ u.email = "doug@example.com"
260
+
224
261
  u.tagged_at.should be_nil
225
- u.semiactivate
262
+ u.semiactivate!
226
263
 
227
264
  u.semiactivated_state_at.should_not be_nil
228
265
  u.tagged_at.should_not be_nil
229
266
  end
230
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
231
308
  end
232
309
 
233
- describe 'aliasing' do
234
- it 'should allow aliasing within the dsl' do
235
- u = User.new(:email => 'doug@example.com')
310
+ describe "aliasing" do
311
+ it "should allow aliasing within the dsl" do
312
+ u = User.new(email: "doug@example.com")
236
313
  u.should respond_to(:active?)
237
314
  u.should respond_to(:inactive?)
238
315
 
@@ -249,11 +326,11 @@ describe Stator::Model do
249
326
  u.should be_active
250
327
  u.should_not be_inactive
251
328
 
252
- User::ACTIVE_STATES.should eql(['activated', 'hyperactivated'])
253
- User::INACTIVE_STATES.should eql(['pending', 'deactivated', 'semiactivated'])
329
+ User::ACTIVE_STATES.should eql(%w[activated hyperactivated])
330
+ User::INACTIVE_STATES.should eql(%w[pending deactivated semiactivated])
254
331
 
255
- User.active.to_sql.gsub(' ', ' ').should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
256
- User.inactive.to_sql.gsub(' ', ' ').should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
332
+ User.active.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('activated', 'hyperactivated')")
333
+ User.inactive.to_sql.gsub(" ", " ").should eq("SELECT users.* FROM users WHERE users.state IN ('pending', 'deactivated', 'semiactivated')")
257
334
  end
258
335
 
259
336
  it "should evaluate inverses correctly" do
@@ -268,7 +345,7 @@ describe Stator::Model do
268
345
  f.should be_house_cleaned
269
346
  end
270
347
 
271
- it 'should namespace aliases just like everything else' do
348
+ it "should namespace aliases just like everything else" do
272
349
  f = Farm.new
273
350
  f.should respond_to(:house_cleaned?)
274
351
 
@@ -278,19 +355,23 @@ describe Stator::Model do
278
355
  f.should be_house_cleaned
279
356
  end
280
357
 
281
- it 'should allow for explicit constant and scope names to be provided' do
358
+ it "should allow for explicit constant and scope names to be provided" do
282
359
  User.should respond_to(:luke_warmers)
283
360
  (!!defined?(User::LUKE_WARMERS)).should eql(true)
284
361
  u = User.new
285
362
  u.should respond_to(:luke_warm?)
286
363
  end
287
364
 
288
- it 'should not create constants or scopes by default' do
365
+ it "should not create constants or scopes by default" do
289
366
  u = User.new
290
367
  u.should respond_to(:iced_tea?)
291
368
  (!!defined?(User::ICED_TEA_STATES)).should eql(false)
292
369
  User.should_not respond_to(:iced_tea)
293
370
  end
294
- end
295
371
 
372
+ it "should determine the full list of states correctly" do
373
+ states = User._stator("").states
374
+ states.should eql(%w[pending activated deactivated semiactivated hyperactivated])
375
+ end
376
+ end
296
377
  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
 
@@ -1,10 +1,9 @@
1
1
  class User < ActiveRecord::Base
2
2
  extend Stator::Model
3
3
 
4
- before_validation :set_tagged_at
4
+ before_save :set_tagged_at
5
5
 
6
- # initial state = pending
7
- stator track: true do
6
+ stator track: true, initial: :pending do
8
7
 
9
8
  transition :activate do
10
9
  from :pending, :semiactivated
@@ -135,6 +134,33 @@ class Zoo < ActiveRecord::Base
135
134
  end
136
135
  end
137
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
138
164
 
139
165
  class Farm < ActiveRecord::Base
140
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.0
4
+ version: 0.3.3
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-10-23 00:00:00.000000000 Z
11
+ date: 2021-03-18 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.7.7
78
+ rubygems_version: 3.0.6
79
79
  signing_key:
80
80
  specification_version: 4
81
81
  summary: The simplest of ActiveRecord state machines