stator 0.3.3 → 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.
data/lib/stator/alias.rb CHANGED
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stator
2
4
  class Alias
5
+ attr_reader :machine, :name, :namespace, :attr_name, :states, :not, :opposite_args, :constant, :scope
3
6
 
4
7
  def initialize(machine, name, options = {})
5
8
  @machine = machine
6
9
  @name = name
7
- @namespace = @machine.namespace
8
- @full_name = [@namespace, @name].compact.join('_')
10
+ @namespace = machine.namespace
9
11
  @states = []
10
12
  @not = false
11
13
  @opposite = nil
@@ -13,8 +15,12 @@ module Stator
13
15
  @scope = options[:scope]
14
16
  end
15
17
 
18
+ def attr_name
19
+ @attr_name ||= generate_attr_name
20
+ end
21
+
16
22
  def is(*args)
17
- @states |= args.map(&:to_s)
23
+ @states |= args.map(&:to_sym)
18
24
  end
19
25
 
20
26
  def is_not(*args)
@@ -22,60 +28,75 @@ module Stator
22
28
  is(*args)
23
29
  end
24
30
 
31
+ alias not? not
32
+
25
33
  def opposite(*args)
26
- @opposite = args
34
+ # set the incoming args for opposite as opposite
35
+ @opposite_args = args
27
36
  end
28
37
 
29
38
  def evaluate
30
39
  generate_methods
31
40
 
32
- if @opposite
33
- op = @machine.state_alias(*@opposite)
41
+ return if opposite_args.blank?
34
42
 
35
- op.is(*@states) if @not
36
- op.is_not(*@states) if !@not
37
- end
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?
38
48
  end
39
49
 
40
- protected
50
+ private
51
+
52
+ def inverse_states
53
+ (machine.states - states).map(&:to_sym)
54
+ end
41
55
 
42
56
  def inferred_constant_name
43
- [@full_name.upcase, @machine.field.to_s.pluralize.upcase].join('_')
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
44
66
  end
45
67
 
46
68
  def generate_methods
69
+ expected_states = (not? ? inverse_states : states)
47
70
 
48
- not_states = (@machine.states - @states)
71
+ if scope
72
+ name = (scope == true ? attr_name : scope)
49
73
 
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
- }
74
+ machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
75
+ scope :#{name}, -> { where(_stator(#{namespace.inspect}).field => #{expected_states}) }
56
76
  EV
57
77
  end
58
78
 
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
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
64
85
  EV
65
86
  else
66
- @machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
67
- #{name} = #{@states.inspect}.freeze
87
+ machine.klass.class_eval <<-EV, __FILE__, __LINE__ + 1
88
+ #{name} = #{states}.freeze
68
89
  EV
69
90
  end
70
91
  end
71
92
 
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)
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)
76
98
  end
77
99
  EV
78
100
  end
79
-
80
101
  end
81
102
  end
@@ -2,40 +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
5
8
 
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
9
+ attr_reader :skip_validations, :skip_transition_tracking, :record, :machine
12
10
 
13
11
  def initialize(machine, record)
14
12
  @machine = machine
15
13
  @record = record
14
+ @skip_transition_tracking = false
16
15
  end
17
16
 
18
17
  def state=(new_value)
19
- @record.send("#{@machine.field}=", new_value)
18
+ record.send("#{machine.field}=", new_value)
20
19
  end
21
20
 
22
21
  def state
23
- @record.send(@machine.field)
22
+ record.send(machine.field)&.to_sym
24
23
  end
25
24
 
26
25
  def state_was(use_previous = false)
27
26
  if use_previous
28
- @record.previous_changes[@machine.field.to_s].try(:[], 0)
27
+ record.previous_changes[machine.field].try(:[], 0).to_sym
29
28
  else
30
- @record.send("#{@machine.field}_was")
29
+ record.send("#{@machine.field}_was")
31
30
  end
32
31
  end
33
32
 
33
+ 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?
37
+ return true if time.nil?
38
+
39
+ record.send(field_name) <= time
40
+ end
41
+
34
42
  def state_changed?(use_previous = false)
35
43
  if use_previous
36
- !!@record.previous_changes[@machine.field.to_s]
44
+ !!record.previous_changes[machine.field.to_s]
37
45
  else
38
- @record.send("#{@machine.field}_changed?")
46
+ record.send("#{machine.field}_changed?")
39
47
  end
40
48
  end
41
49
 
@@ -43,23 +51,20 @@ module Stator
43
51
  return unless state_changed?
44
52
  return if skip_validations
45
53
 
46
- was = state_was
47
- is = state
48
-
49
- if @record.new_record?
50
- invalid_state! unless @machine.matching_transition(::Stator::Transition::ANY, is)
54
+ if record.new_record?
55
+ invalid_state! unless machine.matching_transition(Stator::ANY, state)
51
56
  else
52
- invalid_transition!(was, is) unless @machine.matching_transition(was, is)
57
+ invalid_transition!(state_was, state) unless machine.matching_transition(state_was, state)
53
58
  end
54
59
  end
55
60
 
56
61
  # TODO: i18n
57
62
  def invalid_state!
58
- @record.errors.add(@machine.field, "is not a valid state")
63
+ record.errors.add(machine.field, 'is not a valid state')
59
64
  end
60
65
 
61
66
  def invalid_transition!(was, is)
62
- @record.errors.add(@machine.field, "cannot transition to #{is.inspect} from #{was.inspect}")
67
+ record.errors.add(machine.field, "cannot transition to #{is} from #{was}")
63
68
  end
64
69
 
65
70
  def track_transition
@@ -75,7 +80,7 @@ module Stator
75
80
  state = state.to_s
76
81
  t = t.to_time
77
82
 
78
- state_at = @record.send("#{state}_#{@machine.field}_at")
83
+ state_at = record.send("#{state}_#{machine.field}_at")
79
84
 
80
85
  # if we've never been in the state, the answer is no
81
86
  return false if state_at.nil?
@@ -83,20 +88,18 @@ module Stator
83
88
  # if we came into this state later in life, the answer is no
84
89
  return false if state_at > t
85
90
 
86
- all_states = @machine.states.reverse
91
+ all_states = machine.states.reverse
87
92
 
88
93
  # grab all the states and their timestamps that occur on or after state_at and on or before the time in question
89
- later_states = all_states.map do |s|
94
+ later_states = all_states.filter_map do |s|
90
95
  next if state == s
91
96
 
92
- at = @record.send("#{s}_#{@machine.field}_at")
97
+ at = record.send("#{s}_#{machine.field}_at")
93
98
 
94
- next if at.nil?
95
- next if at < state_at
96
- next if at > t
99
+ next if at.nil? || at < state_at || at > t
97
100
 
98
101
  { state: s, at: at }
99
- end.compact
102
+ end
100
103
 
101
104
  # if there were no states on or after the state_at, the answer is yes
102
105
  return true if later_states.empty?
@@ -107,53 +110,51 @@ module Stator
107
110
  later_states = later_groups[later_group_key]
108
111
 
109
112
  # if the lowest timestamp is the same as the state's timestamp, evaluate based on state index
110
- if later_states[0][:at] == state_at
111
- return all_states.index(state) < all_states.index(later_states[0][:state])
112
- end
113
+ return all_states.index(state) < all_states.index(later_states[0][:state]) if later_states[0][:at] == state_at
113
114
 
114
115
  false
115
116
  end
116
117
 
117
118
  def likely_state_at(t)
118
- @machine.states.reverse.detect { |s| in_state_at?(s, t) }
119
+ machine.states.reverse.detect { |s| in_state_at?(s, t) }
119
120
  end
120
121
 
121
122
  def without_validation
122
- was = @skip_validations
123
+ was = skip_validations
123
124
  @skip_validations = true
124
- yield @record
125
+ yield record
125
126
  ensure
126
127
  @skip_validations = was
127
128
  end
128
129
 
129
130
  def without_transition_tracking
130
- was = @skip_transition_tracking
131
+ was = skip_transition_tracking
131
132
  @skip_transition_tracking = true
132
- yield @record
133
+ yield record
133
134
  ensure
134
135
  @skip_transition_tracking = was
135
136
  end
136
137
 
137
- protected
138
+ private
138
139
 
139
140
  def attempt_to_track_state(state_to_track)
140
141
  return unless state_to_track
141
142
 
142
- _attempt_to_track_change("#{state_to_track}_#{@machine.field}_at")
143
+ _attempt_to_track_change("#{state_to_track}_#{machine.field}_at")
143
144
  end
144
145
 
145
146
  def attempt_to_track_state_changed_timestamp
146
- _attempt_to_track_change("#{@machine.field}_changed_at")
147
+ _attempt_to_track_change("#{machine.field}_changed_at")
147
148
  end
148
149
 
149
150
  def _attempt_to_track_change(field_name)
150
- return unless @record.respond_to?(field_name)
151
- return unless @record.respond_to?("#{field_name}=")
152
- return unless @record.send(field_name.to_s).nil? || state_changed?
153
- return if @record.send("#{field_name}_changed?")
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
154
 
155
- @record.send("#{field_name}=", (Time.zone || Time).now)
156
- end
155
+ return if record.send("#{field_name}_changed?")
157
156
 
157
+ record.send("#{field_name}=", (Time.zone || Time).now)
158
+ end
158
159
  end
159
160
  end
@@ -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,29 +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}
123
+ end
124
+
125
+ def #{attr_name(state)}_state_by?(time)
126
+ _stator_integration(:#{namespace}).state_by?(:#{state}.to_sym, time)
119
127
  end
120
128
  EV
121
129
  end
122
130
  end
123
-
124
-
125
131
  end
126
132
  end