stator 0.8.0 → 0.9.0.beta

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +10 -5
  3. data/.gitignore +1 -0
  4. data/.ruby-version +1 -1
  5. data/Appraisals +12 -15
  6. data/Gemfile +7 -6
  7. data/README.md +4 -20
  8. data/gemfiles/{activerecord_7.2.gemfile → activerecord_5.1.gemfile} +2 -1
  9. data/gemfiles/activerecord_5.1.gemfile.lock +74 -0
  10. data/gemfiles/{activerecord_8.0.gemfile → activerecord_5.2.gemfile} +2 -1
  11. data/gemfiles/activerecord_5.2.gemfile.lock +74 -0
  12. data/gemfiles/{activerecord_7.1.gemfile → activerecord_5.gemfile} +2 -1
  13. data/gemfiles/activerecord_5.gemfile.lock +74 -0
  14. data/gemfiles/activerecord_6.0.gemfile +2 -2
  15. data/gemfiles/activerecord_6.0.gemfile.lock +41 -69
  16. data/gemfiles/activerecord_6.1.gemfile +2 -2
  17. data/gemfiles/activerecord_6.1.gemfile.lock +41 -68
  18. data/gemfiles/activerecord_7.0.gemfile +2 -2
  19. data/gemfiles/activerecord_7.0.gemfile.lock +40 -66
  20. data/lib/stator/alias.rb +51 -30
  21. data/lib/stator/integration.rb +42 -49
  22. data/lib/stator/machine.rb +59 -58
  23. data/lib/stator/model.rb +78 -73
  24. data/lib/stator/transition.rb +61 -60
  25. data/lib/stator/version.rb +3 -5
  26. data/lib/stator.rb +13 -0
  27. data/spec/model_spec.rb +203 -318
  28. data/spec/spec_helper.rb +6 -2
  29. data/spec/support/models.rb +26 -45
  30. data/spec/support/schema.rb +42 -42
  31. data/stator.gemspec +3 -10
  32. metadata +19 -75
  33. data/.github/CODEOWNERS +0 -1
  34. data/.github/dependabot.yml +0 -24
  35. data/gemfiles/activerecord_7.1.gemfile.lock +0 -114
  36. data/gemfiles/activerecord_7.2.gemfile.lock +0 -113
  37. data/gemfiles/activerecord_8.0.gemfile.lock +0 -116
@@ -1,57 +1,65 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stator
2
4
  class Machine
5
+ attr_reader :initial_state, :field, :transitions, :states, :namespace,
6
+ :class_name, :name, :aliases, :options, :tracking_enabled, :klass
7
+
8
+ def self.find_or_create(klass, *kwargs)
9
+ kwargs = kwargs.first
10
+
11
+ klass._stators[kwargs[:namespace]] ||= new(klass, **kwargs)
12
+ end
13
+
14
+ def klass
15
+ @klass ||= class_name.constantize
16
+ end
3
17
 
4
- attr_reader :initial_state
5
- attr_reader :field
6
- attr_reader :transition_names
7
- attr_reader :transitions
8
- attr_reader :states
9
- attr_reader :namespace
18
+ def initialize(klass, *options)
19
+ options = options.first
10
20
 
11
- def initialize(klass, options = {})
12
21
  @class_name = klass.name
13
22
  @field = options[:field] || :state
14
- @namespace = options[:namespace]
23
+ @namespace = (options[:namespace] || Stator.default_namespace).to_sym
15
24
 
16
- @initial_state = options[:initial] && options[:initial].to_s
25
+ @initial_state = options[:initial]&.to_sym
26
+ @states = [initial_state].compact
17
27
  @tracking_enabled = options[:track] || false
18
28
 
19
29
  @transitions = []
20
30
  @aliases = []
21
31
 
22
- # pushed out into their own variables for performance reasons (AR integration can use method missing - see the HelperMethods module)
23
- @transition_names = []
24
- @states = [@initial_state].compact
25
-
26
- @options = options
32
+ @options = options
27
33
  end
28
34
 
29
- def integration(record)
30
- ::Stator::Integration.new(self, record)
35
+ alias tracking_enabled? tracking_enabled
36
+
37
+ def evaluate_dsl(&block)
38
+ instance_eval(&block)
39
+ evaluate
31
40
  end
32
41
 
33
- def get_transition(name)
34
- @transitions.detect{|t| t.name.to_s == name.to_s}
42
+ def integration(record)
43
+ Stator::Integration.new(self, record)
35
44
  end
36
45
 
37
46
  def transition(name, &block)
38
- t = ::Stator::Transition.new(@class_name, name, @namespace)
39
- t.instance_eval(&block) if block_given?
40
-
41
- verify_transition_validity(t)
47
+ Stator::Transition.new(class_name, name, namespace).tap do |t|
48
+ t.instance_eval(&block) if block_given?
42
49
 
43
- @transitions << t
44
- @transition_names |= [t.full_name] unless t.full_name.blank?
45
- @states |= [t.to_state] unless t.to_state.nil?
50
+ verify_transition_validity(t)
46
51
 
47
- t
52
+ @transitions << t
53
+ @states |= [t.to_state] unless t.to_state.nil?
54
+ end
48
55
  end
49
56
 
50
57
  def state_alias(name, options = {}, &block)
51
- a = ::Stator::Alias.new(self, name, options)
52
- a.instance_eval(&block) if block_given?
53
- @aliases << a
54
- a
58
+ Stator::Alias.new(self, name, options).tap do |a|
59
+ # puts "ALIAS: #{a.inspect}"
60
+ a.instance_eval(&block) if block_given?
61
+ @aliases << a
62
+ end
55
63
  end
56
64
 
57
65
  def state(name, &block)
@@ -62,33 +70,31 @@ module Stator
62
70
  end
63
71
  end
64
72
 
65
- def tracking_enabled?
66
- @tracking_enabled
67
- end
68
-
69
73
  def conditional(*states, &block)
70
- _namespace = @namespace
74
+ state_check = proc { states.include?(current_state) }
71
75
 
72
- klass.instance_exec(proc { states.map(&:to_s).include?(self._stator(_namespace).integration(self).state) }, &block)
76
+ klass.instance_exec(state_check, &block)
73
77
  end
74
78
 
75
79
  def matching_transition(from, to)
76
- @transitions.detect do |transition|
77
- transition.valid?(from, to)
78
- end
80
+ transitions.detect { |transition| transition.valid?(from, to) }
79
81
  end
80
82
 
81
83
  def evaluate
82
- @transitions.each(&:evaluate)
83
- @aliases.each(&:evaluate)
84
+ transitions.each(&:evaluate)
85
+ aliases.each(&:evaluate)
84
86
  generate_methods
85
87
  end
86
88
 
87
- def klass
88
- @class_name.constantize
89
- end
89
+ private
90
90
 
91
- protected
91
+ def attr_name(name)
92
+ if namespace == Stator.default_namespace
93
+ name.to_sym
94
+ else
95
+ [namespace, name].compact.join('_').to_sym
96
+ end
97
+ end
92
98
 
93
99
  def verify_transition_validity(transition)
94
100
  verify_state_singularity_of_transition(transition)
@@ -98,34 +104,29 @@ module Stator
98
104
  def verify_state_singularity_of_transition(transition)
99
105
  transition.from_states.each do |from|
100
106
  if matching_transition(from, transition.to_state)
101
- raise "[Stator] another transition already exists which moves #{@class_name} from #{from.inspect} to #{transition.to_state.inspect}"
107
+ raise "[Stator] another transition already exists which moves #{class_name} from #{from} to #{transition}"
102
108
  end
103
109
  end
104
110
  end
105
111
 
106
112
  def verify_name_singularity_of_transition(transition)
107
- if @transitions.detect{|other| transition.name && transition.name == other.name }
108
- raise "[Stator] another transition already exists with the name of #{transition.name.inspect} in the #{@class_name} class"
113
+ if transitions.detect { |other| transition.name && transition.name == other.name }
114
+ raise "[Stator] another transition already exists with the name of #{transition.name} in the #{class_name} class"
109
115
  end
110
116
  end
111
117
 
112
118
  def generate_methods
113
- self.states.each do |state|
114
- method_name = [@namespace, state].compact.join('_')
119
+ states.each do |state|
115
120
  klass.class_eval <<-EV, __FILE__, __LINE__ + 1
116
- def #{method_name}?
117
- integration = self._stator(#{@namespace.inspect}).integration(self)
118
- integration.state == #{state.to_s.inspect}
121
+ def #{attr_name(state)}?
122
+ _stator_integration(:#{namespace}).state.to_sym == :#{state}
119
123
  end
120
124
 
121
- def #{method_name}_state_by?(time)
122
- integration = self._stator(#{@namespace.inspect}).integration(self)
123
- integration.state_by?(#{state.to_s.inspect}, time)
125
+ def #{attr_name(state)}_state_by?(time)
126
+ _stator_integration(:#{namespace}).state_by?(:#{state}.to_sym, time)
124
127
  end
125
128
  EV
126
129
  end
127
130
  end
128
-
129
-
130
131
  end
131
132
  end
data/lib/stator/model.rb CHANGED
@@ -1,113 +1,118 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stator
2
4
  module Model
5
+ extend ActiveSupport::Concern
3
6
 
4
- def stator(options = {}, &block)
5
-
6
- class_attribute :_stators unless respond_to?(:_stators)
7
+ included do
8
+ class_attribute :_stators
9
+ attr_accessor :_stator_integrations
7
10
 
8
- include InstanceMethods unless self.included_modules.include?(InstanceMethods)
9
- include TrackerMethods if options[:track] == true
11
+ validate :_stator_validate_transition
10
12
 
11
13
  self._stators ||= {}
12
14
 
13
- unless self.abstract_class?
14
- f = options[:field] || :state
15
- # rescue nil since the table may not exist yet.
16
- initial = self.columns_hash[f.to_s].default rescue nil
17
- options = options.merge(initial: initial) if initial
18
- end
19
-
20
- machine = (self._stators[options[:namespace].to_s] ||= ::Stator::Machine.new(self, options))
21
-
22
- if block_given?
23
- machine.instance_eval(&block)
24
- machine.evaluate
25
- end
26
-
27
- machine
15
+ before_save :_stator_maybe_track_transition, prepend: true
28
16
  end
29
17
 
30
- def _stator(namespace)
31
- self._stators[namespace.to_s]
32
- end
18
+ class_methods do
19
+ def stator(namespace: nil, field: :state, initial: nil, track: true, &block)
20
+ unless abstract_class?
21
+ # Discover the default value (usually initial) from the table...
22
+ # but rescue nil since the table may not exist yet.
23
+ initial = _determine_initial_stator_state(field)
24
+ end
33
25
 
34
- module TrackerMethods
26
+ opts = { namespace: _stator_namespace(namespace), field: field.to_sym, initial: initial, track: track }
35
27
 
36
- def self.included(base)
37
- base.class_eval do
38
- before_save :_stator_maybe_track_transition, prepend: true
28
+ Stator::Machine.find_or_create(self, **opts).tap do |machine|
29
+ machine.evaluate_dsl(&block) if block_given?
39
30
  end
40
31
  end
41
32
 
42
- def in_state_at?(state, t, namespace = '')
43
- _integration(namespace).in_state_at?(state, t)
33
+ def _stator(namespace)
34
+ self._stators[_stator_namespace(namespace)]
44
35
  end
45
36
 
46
- def likely_state_at(t, namespace = '')
47
- _integration(namespace).likely_state_at(t)
48
- end
37
+ def _stator_namespace(namespace = nil)
38
+ namespace = nil if namespace.blank?
49
39
 
50
- def state_by?(state, t, namespace = '')
51
- _integration(namespace).state_by?(state, t)
40
+ (namespace || Stator.default_namespace).to_sym
52
41
  end
53
42
 
54
- protected
43
+ def _determine_initial_stator_state(field)
44
+ columns_hash[field.to_s].default.to_sym
45
+ rescue StandardError
46
+ nil
47
+ end
48
+ end
55
49
 
56
- def _stator_maybe_track_transition
57
- self._stators.each do |namespace, machine|
58
- next unless machine.tracking_enabled?
50
+ def initialize_dup(other)
51
+ @_stator_integrations = {}
52
+ super
53
+ end
59
54
 
60
- _integration(namespace).track_transition
61
- end
55
+ def without_state_transition_validations(namespace = '')
56
+ _stator_integration(namespace).without_validation do
57
+ yield self
58
+ end
59
+ end
62
60
 
63
- true
61
+ def without_state_transition_tracking(namespace = '')
62
+ _stator_integration(namespace).without_transition_tracking do
63
+ yield self
64
64
  end
65
+ end
65
66
 
67
+ def current_state
68
+ _stator_integration.state&.to_sym
66
69
  end
67
70
 
68
- module InstanceMethods
71
+ def in_state_at?(state, t, namespace = '')
72
+ _stator_integration(namespace).in_state_at?(state, t)
73
+ end
69
74
 
70
- def self.included(base)
71
- base.class_eval do
72
- validate :_stator_validate_transition
73
- end
74
- end
75
+ def likely_state_at(t, namespace = '')
76
+ _stator_integration(namespace).likely_state_at(t)
77
+ end
75
78
 
76
- def initialize_dup(other)
77
- @_integrations = {}
78
- super
79
- end
79
+ def state_by?(state, t, namespace = '')
80
+ _stator_integration(namespace).state_by?(state, t)
81
+ end
80
82
 
81
- def without_state_transition_validations(namespace = '')
82
- _integration(namespace).without_validation do
83
- yield self
84
- end
85
- end
83
+ private
86
84
 
87
- def without_state_transition_tracking(namespace = '')
88
- _integration(namespace).without_transition_tracking do
89
- yield self
90
- end
91
- end
85
+ # core methods
86
+ def _stator(namespace = nil)
87
+ self.class._stator(namespace)
88
+ end
92
89
 
93
- protected
90
+ def _stator_namespace(namespace = nil)
91
+ self.class._stator_namespace(namespace)
92
+ end
94
93
 
95
- def _stator_validate_transition
96
- self._stators.each_key do |namespace|
97
- _integration(namespace).validate_transition
98
- end
99
- end
94
+ def _stator_integration(namespace = nil)
95
+ ns = _stator_namespace(namespace)
100
96
 
101
- def _stator(namespace = '')
102
- self.class._stator(namespace)
97
+ self._stator_integrations ||= {}
98
+ self._stator_integrations[ns] ||= self.class._stator(ns).integration(self)
99
+ end
100
+
101
+ # validation/transitional
102
+ def _stator_validate_transition
103
+ self._stators.each_key do |namespace|
104
+ _stator_integration(namespace).validate_transition
103
105
  end
106
+ end
107
+
108
+ def _stator_maybe_track_transition
109
+ self._stators.each do |namespace, machine|
110
+ next unless machine.tracking_enabled?
104
111
 
105
- def _integration(namespace = '')
106
- @_integrations ||= {}
107
- @_integrations[namespace] ||= _stator(namespace).integration(self)
108
- @_integrations[namespace]
112
+ _stator_integration(namespace).track_transition
109
113
  end
110
114
 
115
+ true
111
116
  end
112
117
  end
113
118
  end
@@ -1,44 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stator
2
4
  class Transition
3
-
4
- ANY = '__any__'
5
-
6
- attr_reader :name
7
- attr_reader :full_name
5
+ attr_reader :namespace, :name, :attr_name, :from_states, :to_state, :class_name, :callbacks
8
6
 
9
7
  def initialize(class_name, name, namespace = nil)
10
- @class_name = class_name
11
- @name = name
12
- @namespace = namespace
13
- @full_name = [@namespace, @name].compact.join('_') if @name
14
- @froms = []
15
- @to = nil
16
- @callbacks = {}
8
+ @class_name = class_name
9
+ @name = name&.to_sym
10
+ @namespace = namespace&.to_sym
11
+ @from_states = []
12
+ @to_state = nil
13
+ @callbacks = {}
17
14
  end
18
15
 
19
- def from(*froms)
20
- @froms |= froms.map{|f| f.try(:to_s) } # nils are ok
16
+ def attr_name
17
+ @attr_name ||= generate_attr_name
21
18
  end
22
19
 
23
- def to(to)
24
- @to = to.to_s
20
+ def from_states(*new_froms)
21
+ @from_states |= new_froms
25
22
  end
23
+ alias from from_states
26
24
 
27
- def to_state
28
- @to
29
- end
30
-
31
- def from_states
32
- @froms
25
+ def to(new_to)
26
+ @to_state = new_to
33
27
  end
34
28
 
35
29
  def can?(current_state)
36
- @froms.include?(current_state) || @froms.include?(ANY) || current_state == ANY
30
+ from_states.include?(current_state) || from_states.include?(Stator::ANY) || current_state == Stator::ANY
37
31
  end
38
32
 
39
- def valid?(from, to)
40
- can?(from) &&
41
- (@to == to || @to == ANY || to == ANY)
33
+ def valid?(from_check, to_check)
34
+ from_check = from_check&.to_sym # coming from the database, i suspect
35
+
36
+ can?(from_check) && (to_check == to_state || to_check == ANY || to_state == ANY)
42
37
  end
43
38
 
44
39
  def conditional(options = {}, &block)
@@ -46,79 +41,85 @@ module Stator
46
41
  end
47
42
 
48
43
  def any
49
- ANY
44
+ Stator::ANY
50
45
  end
51
46
 
52
47
  def evaluate
53
- generate_methods unless @full_name.blank?
48
+ generate_methods if attr_name.present?
54
49
  end
55
50
 
56
- protected
51
+ private
57
52
 
58
53
  def klass
59
- @class_name.constantize
54
+ class_name.constantize
55
+ end
56
+
57
+ def generate_attr_name
58
+ if namespace == Stator.default_namespace
59
+ name
60
+ else
61
+ [namespace, name].compact.join('_').to_sym
62
+ end
60
63
  end
61
64
 
62
65
  def callbacks(kind)
63
- @callbacks[kind] || []
66
+ callbacks[kind] || []
64
67
  end
65
68
 
66
69
  def conditional_block(options = {})
67
70
  options[:use_previous] ||= false
68
71
 
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
- )
72
+ _namespace = namespace
73
+ _froms = from_states
74
+ _to = to_state
75
+
76
+ proc do
77
+ integration = self.class._stator(_namespace).integration(self)
78
+
79
+ integration.state_changed?(options[:use_previous]) &&
80
+ _froms.include?(integration.state_was(options[:use_previous])) ||
81
+ _froms.include?(Stator::ANY) &&
82
+ integration.state == _to || _to == Stator::ANY
83
83
  end
84
84
  end
85
85
 
86
86
  def generate_methods
87
87
  klass.class_eval <<-EV, __FILE__, __LINE__ + 1
88
- def #{@full_name}(should_save = true)
89
- integration = _integration(#{@namespace.to_s.inspect})
88
+ def #{attr_name}(should_save = true)
89
+ integration = _stator_integration(:#{namespace})
90
90
 
91
- unless can_#{@full_name}?
92
- integration.invalid_transition!(integration.state, #{@to.inspect}) if should_save
91
+ unless can_#{attr_name}?
92
+ integration.invalid_transition!(integration.state, :#{to_state}) if should_save
93
93
  return false
94
94
  end
95
95
 
96
- integration.state = #{@to.inspect}
96
+ integration.state = :#{to_state}
97
+
97
98
  self.save if should_save
98
99
  end
99
100
 
100
- def #{@full_name}!
101
- integration = _integration(#{@namespace.to_s.inspect})
101
+ def #{attr_name}!
102
+ integration = _stator_integration(:#{namespace})
102
103
 
103
- unless can_#{@full_name}?
104
- integration.invalid_transition!(integration.state, #{@to.inspect})
104
+ unless can_#{attr_name}?
105
+ integration.invalid_transition!(integration.state, :#{to_state})
105
106
  raise ActiveRecord::RecordInvalid.new(self)
106
107
  end
107
108
 
108
- integration.state = #{@to.inspect}
109
+ integration.state = :#{to_state}
109
110
  self.save!
110
111
  end
111
112
 
112
- def can_#{@full_name}?
113
- integration = _integration(#{@namespace.to_s.inspect})
113
+ def can_#{attr_name}?
114
+ integration = _stator_integration(:#{namespace})
114
115
  return true if integration.skip_validations
115
116
 
116
- machine = self._stator(#{@namespace.to_s.inspect})
117
- transition = machine.transitions.detect{|t| t.full_name.to_s == #{@full_name.inspect}.to_s }
117
+ machine = self._stator(:#{namespace})
118
+ transition = machine.transitions.detect { |t| t.attr_name == :#{attr_name} }
119
+
118
120
  transition.can?(integration.state)
119
121
  end
120
122
  EV
121
123
  end
122
-
123
124
  end
124
125
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stator
4
-
5
4
  MAJOR = 0
6
- MINOR = 8
5
+ MINOR = 9
7
6
  PATCH = 0
8
- PRERELEASE = nil
9
-
10
- VERSION = [MAJOR, MINOR, PATCH, PRERELEASE].compact.join(".")
7
+ PRERELEASE = "beta"
11
8
 
9
+ VERSION = [MAJOR, MINOR, PATCH, PRERELEASE].compact.join('.')
12
10
  end
data/lib/stator.rb CHANGED
@@ -1,6 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'stator/version'
2
4
  require 'stator/alias'
3
5
  require 'stator/integration'
4
6
  require 'stator/machine'
5
7
  require 'stator/model'
6
8
  require 'stator/transition'
9
+
10
+ require 'active_support/concern'
11
+ require 'debug'
12
+
13
+ module Stator
14
+ ANY = :__ANY__
15
+
16
+ def self.default_namespace
17
+ ENV.fetch('STATOR_NAMESPACE', :default).to_sym
18
+ end
19
+ end