stator 0.9.0.beta → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11dfad97bef44bb60c060b086dce2f327f2fca47d309510337533f31f62e105f
4
- data.tar.gz: 578888569b569b4e35c7c180ed3ddfe96ea4547f296394c2c81fbc44bee72105
3
+ metadata.gz: 69cde7f0870cc5691869d9e3dbc94c886a74472ec0c858ded58d7f4fde0b5ac1
4
+ data.tar.gz: 9d72a8142353d239883d4c0d244ca1ef9d46e9dde2bf602e19a7c28e53951e3a
5
5
  SHA512:
6
- metadata.gz: 959635a8a2eebce12473a7f79844e25de1038fe3a1e692fa97e5d1edc430778a3148a4a7b6ca2e488f248c318a19d74fb69c489c28928ff44731c0e3e8319823
7
- data.tar.gz: a13fa8a216bc2fd590d1abea7be2eab258a5e29b58c0017314a083938b5c6f6f0acebf3179b5c4cf7b122dbc2a62ba1342bdc0937fed82365ad2b23d49ce3201
6
+ metadata.gz: 29bcc2f18487e5d081e8270646b6a5b612f04aae32c7d0341dda09bf1d90440e6f0788215553ea0d3f86528d8a5593df61d9cb62790fe586af49202bc34db7f1
7
+ data.tar.gz: 354327de6f695f02c8dffd2b9ed1a6a717c49cb88628a1b1edd7120e152348c6c2aa39f010018d05cb9f0638b3917afd811c3dce49a840996d1881a003ec985f
data/lib/stator/alias.rb CHANGED
@@ -1,13 +1,11 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Stator
4
2
  class Alias
5
- attr_reader :machine, :name, :namespace, :attr_name, :states, :not, :opposite_args, :constant, :scope
6
3
 
7
4
  def initialize(machine, name, options = {})
8
5
  @machine = machine
9
6
  @name = name
10
- @namespace = machine.namespace
7
+ @namespace = @machine.namespace
8
+ @full_name = [@namespace, @name].compact.join('_')
11
9
  @states = []
12
10
  @not = false
13
11
  @opposite = nil
@@ -15,12 +13,8 @@ module Stator
15
13
  @scope = options[:scope]
16
14
  end
17
15
 
18
- def attr_name
19
- @attr_name ||= generate_attr_name
20
- end
21
-
22
16
  def is(*args)
23
- @states |= args.map(&:to_sym)
17
+ @states |= args.map(&:to_s)
24
18
  end
25
19
 
26
20
  def is_not(*args)
@@ -28,75 +22,60 @@ module Stator
28
22
  is(*args)
29
23
  end
30
24
 
31
- alias not? not
32
-
33
25
  def opposite(*args)
34
- # set the incoming args for opposite as opposite
35
- @opposite_args = args
26
+ @opposite = args
36
27
  end
37
28
 
38
29
  def evaluate
39
30
  generate_methods
40
31
 
41
- return if opposite_args.blank?
32
+ if @opposite
33
+ op = @machine.state_alias(*@opposite)
42
34
 
43
- # this will generate the alias for the opposite
44
- op = machine.state_alias(*opposite_args)
45
-
46
- op.is(*states) if not?
47
- op.is_not(*states) unless not?
35
+ op.is(*@states) if @not
36
+ op.is_not(*@states) if !@not
37
+ end
48
38
  end
49
39
 
50
- private
51
-
52
- def inverse_states
53
- (machine.states - states).map(&:to_sym)
54
- end
40
+ protected
55
41
 
56
42
  def inferred_constant_name
57
- [attr_name.upcase, machine.field.to_s.pluralize.upcase].join('_')
58
- end
59
-
60
- def generate_attr_name
61
- if namespace == Stator.default_namespace
62
- name
63
- else
64
- [namespace, name].compact.join('_').to_sym
65
- end
43
+ [@full_name.upcase, @machine.field.to_s.pluralize.upcase].join('_')
66
44
  end
67
45
 
68
46
  def generate_methods
69
- expected_states = (not? ? inverse_states : states)
70
47
 
71
- if scope
72
- name = (scope == true ? attr_name : scope)
48
+ not_states = (@machine.states - @states)
73
49
 
74
- machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
75
- scope :#{name}, -> { where(_stator(#{namespace.inspect}).field => #{expected_states}) }
50
+ if @scope
51
+ name = @scope == true ? @full_name : @scope
52
+ @machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
53
+ scope #{name.inspect}, lambda {
54
+ where(_stator(#{@namespace.inspect}).field => #{(@not ? not_states : @states).inspect})
55
+ }
76
56
  EV
77
57
  end
78
58
 
79
- if constant
80
- name = (constant == true ? inferred_constant_name : constant.to_s.upcase)
81
-
82
- if not?
83
- machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
84
- #{name} = #{inverse_states}.freeze
59
+ if @constant
60
+ name = @constant == true ? inferred_constant_name : @constant.to_s.upcase
61
+ if @not
62
+ @machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
63
+ #{name} = #{not_states.inspect}.freeze
85
64
  EV
86
65
  else
87
- machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
88
- #{name} = #{states}.freeze
66
+ @machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
67
+ #{name} = #{@states.inspect}.freeze
89
68
  EV
90
69
  end
91
70
  end
92
71
 
93
- machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
94
- def #{attr_name}?
95
- integration = _stator_integration(:#{namespace})
96
-
97
- #{expected_states}.include?(integration.state.to_sym)
72
+ @machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
73
+ def #{@full_name}?
74
+ integration = _stator(#{@namespace.inspect}).integration(self)
75
+ #{(@not ? not_states : @states).inspect}.include?(integration.state)
98
76
  end
99
77
  EV
100
78
  end
79
+
101
80
  end
102
81
  end
@@ -2,48 +2,48 @@
2
2
 
3
3
  module Stator
4
4
  class Integration
5
- delegate :states, to: :machine
6
- delegate :transitions, to: :machine
7
- delegate :namespace, to: :machine
8
5
 
9
- attr_reader :skip_validations, :skip_transition_tracking, :record, :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
10
12
 
11
13
  def initialize(machine, record)
12
14
  @machine = machine
13
15
  @record = record
14
- @skip_transition_tracking = false
15
16
  end
16
17
 
17
18
  def state=(new_value)
18
- record.send("#{machine.field}=", new_value)
19
+ @record.send("#{@machine.field}=", new_value)
19
20
  end
20
21
 
21
22
  def state
22
- record.send(machine.field)&.to_sym
23
+ @record.send(@machine.field)
23
24
  end
24
25
 
25
26
  def state_was(use_previous = false)
26
27
  if use_previous
27
- record.previous_changes[machine.field].try(:[], 0).to_sym
28
+ @record.attribute_before_last_save(@machine.field)
28
29
  else
29
- record.send("#{@machine.field}_was")
30
+ @record.attribute_in_database(@machine.field)
30
31
  end
31
32
  end
32
33
 
33
34
  def state_by?(state, time)
34
- field_name = "#{state}_#{machine.field}_at"
35
- return false unless record.respond_to?(field_name)
36
- return false if record.send(field_name).nil?
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?
37
38
  return true if time.nil?
38
-
39
- record.send(field_name) <= time
39
+ @record.send(field_name) <= time
40
40
  end
41
41
 
42
42
  def state_changed?(use_previous = false)
43
43
  if use_previous
44
- !!record.previous_changes[machine.field.to_s]
44
+ @record.saved_change_to_attribute?(@machine.field)
45
45
  else
46
- record.send("#{machine.field}_changed?")
46
+ @record.will_save_change_to_attribute?(@machine.field)
47
47
  end
48
48
  end
49
49
 
@@ -51,20 +51,23 @@ module Stator
51
51
  return unless state_changed?
52
52
  return if skip_validations
53
53
 
54
- if record.new_record?
55
- invalid_state! unless machine.matching_transition(Stator::ANY, state)
54
+ was = state_was
55
+ is = state
56
+
57
+ if @record.new_record?
58
+ invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
56
59
  else
57
- invalid_transition!(state_was, state) unless machine.matching_transition(state_was, state)
60
+ invalid_transition!(was, is) unless @machine.matching_transition(was, is)
58
61
  end
59
62
  end
60
63
 
61
64
  # TODO: i18n
62
65
  def invalid_state!
63
- record.errors.add(machine.field, 'is not a valid state')
66
+ @record.errors.add(@machine.field, "is not a valid state")
64
67
  end
65
68
 
66
69
  def invalid_transition!(was, is)
67
- record.errors.add(machine.field, "cannot transition to #{is} from #{was}")
70
+ @record.errors.add(@machine.field, "cannot transition to #{is.inspect} from #{was.inspect}")
68
71
  end
69
72
 
70
73
  def track_transition
@@ -80,7 +83,7 @@ module Stator
80
83
  state = state.to_s
81
84
  t = t.to_time
82
85
 
83
- state_at = record.send("#{state}_#{machine.field}_at")
86
+ state_at = @record.send("#{state}_#{@machine.field}_at")
84
87
 
85
88
  # if we've never been in the state, the answer is no
86
89
  return false if state_at.nil?
@@ -88,18 +91,20 @@ module Stator
88
91
  # if we came into this state later in life, the answer is no
89
92
  return false if state_at > t
90
93
 
91
- all_states = machine.states.reverse
94
+ all_states = @machine.states.reverse
92
95
 
93
96
  # grab all the states and their timestamps that occur on or after state_at and on or before the time in question
94
- later_states = all_states.filter_map do |s|
97
+ later_states = all_states.map do |s|
95
98
  next if state == s
96
99
 
97
- at = record.send("#{s}_#{machine.field}_at")
100
+ at = @record.send("#{s}_#{@machine.field}_at")
98
101
 
99
- next if at.nil? || at < state_at || at > t
102
+ next if at.nil?
103
+ next if at < state_at
104
+ next if at > t
100
105
 
101
106
  { state: s, at: at }
102
- end
107
+ end.compact
103
108
 
104
109
  # if there were no states on or after the state_at, the answer is yes
105
110
  return true if later_states.empty?
@@ -110,51 +115,53 @@ module Stator
110
115
  later_states = later_groups[later_group_key]
111
116
 
112
117
  # if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
113
- return all_states.index(state) < all_states.index(later_states[0][:state]) if later_states[0][:at] == state_at
118
+ if later_states[0][:at] == state_at
119
+ return all_states.index(state) < all_states.index(later_states[0][:state])
120
+ end
114
121
 
115
122
  false
116
123
  end
117
124
 
118
125
  def likely_state_at(t)
119
- machine.states.reverse.detect { |s| in_state_at?(s, t) }
126
+ @machine.states.reverse.detect { |s| in_state_at?(s, t) }
120
127
  end
121
128
 
122
129
  def without_validation
123
- was = skip_validations
130
+ was = @skip_validations
124
131
  @skip_validations = true
125
- yield record
132
+ yield @record
126
133
  ensure
127
134
  @skip_validations = was
128
135
  end
129
136
 
130
137
  def without_transition_tracking
131
- was = skip_transition_tracking
138
+ was = @skip_transition_tracking
132
139
  @skip_transition_tracking = true
133
- yield record
140
+ yield @record
134
141
  ensure
135
142
  @skip_transition_tracking = was
136
143
  end
137
144
 
138
- private
145
+ protected
139
146
 
140
147
  def attempt_to_track_state(state_to_track)
141
148
  return unless state_to_track
142
149
 
143
- _attempt_to_track_change("#{state_to_track}_#{machine.field}_at")
150
+ _attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
144
151
  end
145
152
 
146
153
  def attempt_to_track_state_changed_timestamp
147
- _attempt_to_track_change("#{machine.field}_changed_at")
154
+ _attempt_to_track_change("#{@machine.field}_changed_at")
148
155
  end
149
156
 
150
157
  def _attempt_to_track_change(field_name)
151
- return unless record.respond_to?(field_name)
152
- return unless record.respond_to?("#{field_name}=")
153
- return unless record.send(field_name.to_s).nil? || state_changed?
154
-
155
- return if record.send("#{field_name}_changed?")
158
+ return unless @record.respond_to?(field_name)
159
+ return unless @record.respond_to?("#{field_name}=")
160
+ return unless @record.send(field_name.to_s).nil? || state_changed?
161
+ return if @record.will_save_change_to_attribute?(field_name) && !@record.send(field_name.to_s).nil?
156
162
 
157
- record.send("#{field_name}=", (Time.zone || Time).now)
163
+ @record.send("#{field_name}=", (Time.zone || Time).now)
158
164
  end
165
+
159
166
  end
160
167
  end
@@ -1,65 +1,57 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Stator
4
2
  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
17
3
 
18
- def initialize(klass, *options)
19
- options = options.first
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
20
10
 
11
+ def initialize(klass, options = {})
21
12
  @class_name = klass.name
22
13
  @field = options[:field] || :state
23
- @namespace = (options[:namespace] || Stator.default_namespace).to_sym
14
+ @namespace = options[:namespace]
24
15
 
25
- @initial_state = options[:initial]&.to_sym
26
- @states = [initial_state].compact
16
+ @initial_state = options[:initial] && options[:initial].to_s
27
17
  @tracking_enabled = options[:track] || false
28
18
 
29
19
  @transitions = []
30
20
  @aliases = []
31
21
 
32
- @options = options
33
- end
34
-
35
- alias tracking_enabled? tracking_enabled
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
36
25
 
37
- def evaluate_dsl(&block)
38
- instance_eval(&block)
39
- evaluate
26
+ @options = options
40
27
  end
41
28
 
42
29
  def integration(record)
43
- Stator::Integration.new(self, record)
30
+ ::Stator::Integration.new(self, record)
31
+ end
32
+
33
+ def get_transition(name)
34
+ @transitions.detect{|t| t.name.to_s == name.to_s}
44
35
  end
45
36
 
46
37
  def transition(name, &block)
47
- Stator::Transition.new(class_name, name, namespace).tap do |t|
48
- t.instance_eval(&block) if block_given?
38
+ t = ::Stator::Transition.new(@class_name, name, @namespace)
39
+ t.instance_eval(&block) if block_given?
49
40
 
50
- verify_transition_validity(t)
41
+ verify_transition_validity(t)
51
42
 
52
- @transitions << t
53
- @states |= [t.to_state] unless t.to_state.nil?
54
- end
43
+ @transitions << t
44
+ @transition_names |= [t.full_name] unless t.full_name.blank?
45
+ @states |= [t.to_state] unless t.to_state.nil?
46
+
47
+ t
55
48
  end
56
49
 
57
50
  def state_alias(name, options = {}, &block)
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
51
+ a = ::Stator::Alias.new(self, name, options)
52
+ a.instance_eval(&block) if block_given?
53
+ @aliases << a
54
+ a
63
55
  end
64
56
 
65
57
  def state(name, &block)
@@ -70,32 +62,34 @@ module Stator
70
62
  end
71
63
  end
72
64
 
65
+ def tracking_enabled?
66
+ @tracking_enabled
67
+ end
68
+
73
69
  def conditional(*states, &block)
74
- state_check = proc { states.include?(current_state) }
70
+ _namespace = @namespace
75
71
 
76
- klass.instance_exec(state_check, &block)
72
+ klass.instance_exec(proc { states.map(&:to_s).include?(self._stator(_namespace).integration(self).state) }, &block)
77
73
  end
78
74
 
79
75
  def matching_transition(from, to)
80
- transitions.detect { |transition| transition.valid?(from, to) }
76
+ @transitions.detect do |transition|
77
+ transition.valid?(from, to)
78
+ end
81
79
  end
82
80
 
83
81
  def evaluate
84
- transitions.each(&:evaluate)
85
- aliases.each(&:evaluate)
82
+ @transitions.each(&:evaluate)
83
+ @aliases.each(&:evaluate)
86
84
  generate_methods
87
85
  end
88
86
 
89
- private
90
-
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
87
+ def klass
88
+ @class_name.constantize
97
89
  end
98
90
 
91
+ protected
92
+
99
93
  def verify_transition_validity(transition)
100
94
  verify_state_singularity_of_transition(transition)
101
95
  verify_name_singularity_of_transition(transition)
@@ -104,29 +98,34 @@ module Stator
104
98
  def verify_state_singularity_of_transition(transition)
105
99
  transition.from_states.each do |from|
106
100
  if matching_transition(from, transition.to_state)
107
- raise "[Stator] another transition already exists which moves #{class_name} from #{from} to #{transition}"
101
+ raise "[Stator] another transition already exists which moves #{@class_name} from #{from.inspect} to #{transition.to_state.inspect}"
108
102
  end
109
103
  end
110
104
  end
111
105
 
112
106
  def verify_name_singularity_of_transition(transition)
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"
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"
115
109
  end
116
110
  end
117
111
 
118
112
  def generate_methods
119
- states.each do |state|
113
+ self.states.each do |state|
114
+ method_name = [@namespace, state].compact.join('_')
120
115
  klass.class_eval <<-EV, __FILE__, __LINE__ + 1
121
- def #{attr_name(state)}?
122
- _stator_integration(:#{namespace}).state.to_sym == :#{state}
116
+ def #{method_name}?
117
+ integration = self._stator(#{@namespace.inspect}).integration(self)
118
+ integration.state == #{state.to_s.inspect}
123
119
  end
124
120
 
125
- def #{attr_name(state)}_state_by?(time)
126
- _stator_integration(:#{namespace}).state_by?(:#{state}.to_sym, time)
121
+ def #{method_name}_state_by?(time)
122
+ integration = self._stator(#{@namespace.inspect}).integration(self)
123
+ integration.state_by?(#{state.to_s.inspect}, time)
127
124
  end
128
125
  EV
129
126
  end
130
127
  end
128
+
129
+
131
130
  end
132
131
  end