stator 0.3.3 → 0.9.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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