stator 0.2.0 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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