sequel-state-machine 1.1.0 → 1.2.0

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: 32bf94c0e52e6d2fcaf7593f02f4834f689ee462a378dfc7d561e3e29db79258
4
- data.tar.gz: a99a5e2ccd052cecd228ccc7c702bde70b675b86afac2fbf2b69d95432a9b8b5
3
+ metadata.gz: 3f0c0bd5c716d726e9e8ba679d545038637a3ed7da8a569918e24725d0558021
4
+ data.tar.gz: dcba20e8cb5eb3fbcbad3e70e5e5d0662d3515c44ebb6fa033efc5ae4f1e7a02
5
5
  SHA512:
6
- metadata.gz: 10d259cf6ff25cdabe78ef6699ff6fa9be62de332cd9c45901b5295be687159cbf10e7738426ab94c0f2f4f2c0af3cccf44d3ba371c026f30e00bb6677910747
7
- data.tar.gz: 349b9378af67e386b705d9d1d6e26ca2a48ff953b322f1a47dc40da308ab240e289d1042b53acbaaf2525df408a9c05be5fd62c33b4986b28452a4d51351ced5
6
+ metadata.gz: 78367e0296561bc60d3474dad00650cf1865bd50ee8c96eef1e430c24da24b47ab9f2a70c21f2560a1417ec014b0437e4cbc374c276ab8f18a1f162f4e1dfe5c
7
+ data.tar.gz: 48c59a255b6d6c4736ceed4e75ab8f0bfe59363cab146ae989c2928d9560f829ea62da3cae3452a1006a6e19a7c8600ced96847ab9f453f74cc854cf51c39841
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "sequel"
4
4
  require "sequel/model"
5
+ require "set"
5
6
  require "state_machines/sequel"
6
7
 
7
8
  module Sequel
@@ -14,19 +15,21 @@ module Sequel
14
15
  end
15
16
 
16
17
  def self.configure(model, opts={})
17
- col = opts.is_a?(Symbol) ? opts : nil
18
+ (opts = {status_column: opts}) if opts.is_a?(Symbol)
18
19
  model.instance_eval do
19
20
  # See state_machine_status_column.
20
21
  # We must defer defaulting the value in case the plugin
21
22
  # comes ahead of the state machine (we can see a valid state machine,
22
23
  # but the attribute/name will always be :state, due to some configuration
23
24
  # order of operations).
24
- @sequel_state_machine_status_column = col if col
25
+ @sequel_state_machine_status_column = opts[:status_column] if opts[:status_column]
26
+ @sequel_state_machine_audit_logs_association = opts[:audit_logs_association] || :audit_logs
25
27
  end
26
28
  end
27
29
 
28
30
  module InstanceMethods
29
- private def state_machine_status_column
31
+ private def state_machine_status_column(machine=nil)
32
+ return machine if machine
30
33
  col = self.class.instance_variable_get(:@sequel_state_machine_status_column)
31
34
  return col unless col.nil?
32
35
  if self.respond_to?(:_state_value_attr)
@@ -37,67 +40,81 @@ module Sequel
37
40
  msg = "Model must extend StateMachines::MacroMethods and have one state_machine."
38
41
  raise InvalidConfiguration, msg
39
42
  end
40
- if self.class.state_machines.length > 1
41
- msg = "Cannot use sequel-state-machine with multiple state machines. " \
42
- "Please file an issue at https://github.com/lithictech/sequel-state-machine/issues " \
43
- "if you need this capability."
44
- raise InvalidConfiguration, msg
43
+ if self.class.state_machines.length > 1 && machine.nil?
44
+ msg = "You must provide the :machine keyword argument when working multiple state machines."
45
+ raise ArgumentError, msg
45
46
  end
46
47
  self.class.instance_variable_set(:@sequel_state_machine_status_column, self.class.state_machine.attribute)
47
48
  end
48
49
 
49
- private def state_machine_status
50
- return self.send(self.state_machine_status_column)
51
- end
52
-
53
- def sequel_state_machine_status
54
- return state_machine_status
50
+ def sequel_state_machine_status(machine=nil)
51
+ return self.send(self.state_machine_status_column(machine))
55
52
  end
56
53
 
57
54
  def new_audit_log
58
- audit_log_assoc = self.class.association_reflections[:audit_logs]
59
- model = Kernel.const_get(audit_log_assoc[:class_name])
60
- return model.new
55
+ assoc_name = self.class.instance_variable_get(:@sequel_state_machine_audit_logs_association)
56
+ unless (audit_log_assoc = self.class.association_reflections[assoc_name])
57
+ msg = "Association for audit logs '#{assoc_name}' does not exist. " \
58
+ "Your model must have 'one_to_many :audit_logs' for its audit log lines, " \
59
+ "or pass the :audit_logs_association parameter to the plugin to define its association name."
60
+ raise InvalidConfiguration, msg
61
+ end
62
+ audit_log_cls = audit_log_assoc[:class] || Kernel.const_get(audit_log_assoc[:class_name])
63
+ return audit_log_cls.new
61
64
  end
62
65
 
63
- def current_audit_log
64
- if @current_audit_log.nil?
66
+ def current_audit_log(machine: nil)
67
+ @current_audit_logs ||= {}
68
+ alog = @current_audit_logs[machine]
69
+ if alog.nil?
65
70
  StateMachines::Sequel.log(self, :debug, "preparing_audit_log", {})
66
- @current_audit_log = self.new_audit_log
67
- @current_audit_log.reason = ""
71
+ alog = self.new_audit_log
72
+ if machine
73
+ machine_name_col = alog.class.state_machine_column_mappings[:machine_name]
74
+ unless alog.respond_to?(machine_name_col)
75
+ msg = "Audit logs must have a :machine_name field for multi-machine models or if specifying :machine."
76
+ raise InvalidConfiguration, msg
77
+ end
78
+ alog.sequel_state_machine_set(:machine_name, machine)
79
+ end
80
+ @current_audit_logs[machine] = alog
81
+ alog.sequel_state_machine_set(:reason, "")
68
82
  end
69
- return @current_audit_log
83
+ return alog
70
84
  end
71
85
 
72
86
  def commit_audit_log(transition)
73
- StateMachines::Sequel.log(self, :debug, "committing_audit_log", {transition: transition})
74
- current = self.current_audit_log
87
+ machine = self.class.state_machines.length > 1 ? transition.machine.name : nil
88
+ StateMachines::Sequel.log(
89
+ self, :debug, "committing_audit_log", {transition: transition, state_machine: machine},
90
+ )
91
+ current = self.current_audit_log(machine: machine)
75
92
 
76
93
  last_saved = self.audit_logs.find do |a|
77
- a.event == transition.event.to_s &&
78
- a.from_state == transition.from &&
79
- a.to_state == transition.to
94
+ a.sequel_state_machine_get(:event) == transition.event.to_s &&
95
+ a.sequel_state_machine_get(:from_state) == transition.from &&
96
+ a.sequel_state_machine_get(:to_state) == transition.to
80
97
  end
81
98
  if last_saved
82
99
  StateMachines::Sequel.log(self, :debug, "updating_audit_log", {audit_log_id: last_saved.id})
83
- last_saved.update(
100
+ last_saved.update(**last_saved.sequel_state_machine_map_columns(
84
101
  at: Time.now,
85
102
  actor: StateMachines::Sequel.current_actor,
86
103
  messages: current.messages,
87
104
  reason: current.reason,
88
- )
105
+ ))
89
106
  else
90
107
  StateMachines::Sequel.log(self, :debug, "creating_audit_log", {})
91
- current.set(
108
+ current.set(**current.sequel_state_machine_map_columns(
92
109
  at: Time.now,
93
110
  actor: StateMachines::Sequel.current_actor,
94
111
  event: transition.event.to_s,
95
112
  from_state: transition.from,
96
113
  to_state: transition.to,
97
- )
114
+ ))
98
115
  self.add_audit_log(current)
99
116
  end
100
- @current_audit_log = nil
117
+ @current_audit_logs[machine] = nil
101
118
  end
102
119
 
103
120
  def audit(message, reason: nil)
@@ -110,21 +127,31 @@ module Sequel
110
127
  audlog.messages += (audlog.messages.empty? ? message : (message + "\n"))
111
128
  end
112
129
  audlog.reason = reason if reason
130
+ return audlog
113
131
  end
114
132
 
115
- def audit_one_off(event, messages, reason: nil)
133
+ def audit_one_off(event, messages, reason: nil, machine: nil)
116
134
  messages = [messages] unless messages.respond_to?(:to_ary)
117
135
  audlog = self.new_audit_log
118
- audlog.set(
136
+ mapped_values = audlog.sequel_state_machine_map_columns(
119
137
  at: Time.now,
120
138
  event: event,
121
- from_state: self.state_machine_status,
122
- to_state: self.state_machine_status,
139
+ from_state: self.sequel_state_machine_status(machine),
140
+ to_state: self.sequel_state_machine_status(machine),
123
141
  messages: audlog.class.state_machine_messages_supports_array ? messages : messages.join("\n"),
124
142
  reason: reason || "",
125
143
  actor: StateMachines::Sequel.current_actor,
144
+ machine_name: machine,
126
145
  )
127
- self.add_audit_log(audlog)
146
+ audlog.set(mapped_values)
147
+ return self.add_audit_log(audlog)
148
+ end
149
+
150
+ # Return audit logs for the given state machine name.
151
+ # Only useful for multi-state-machine models.
152
+ def audit_logs_for(machine)
153
+ lines = self.send(self.class.instance_variable_get(:@sequel_state_machine_audit_logs_association))
154
+ return lines.select { |ln| ln.sequel_state_machine_get(:machine_name) == machine.to_s }
128
155
  end
129
156
 
130
157
  # Send event with arguments inside of a transaction, save the changes to the receiver,
@@ -159,10 +186,12 @@ module Sequel
159
186
  end
160
187
 
161
188
  # Return true if the given event can be transitioned into by the current state.
162
- def valid_state_path_through?(event)
163
- current_state_str = self.state_machine_status.to_s
189
+ def valid_state_path_through?(event, machine: nil)
190
+ current_state_str = self.sequel_state_machine_status(machine).to_s
164
191
  current_state_sym = current_state_str.to_sym
165
- event_obj = self.class.state_machine.events[event] or raise "Invalid event #{event}"
192
+ sm = find_state_machine(machine)
193
+ event_obj = sm.events[event] or
194
+ raise ArgumentError, "Invalid event #{event} (available #{sm.name} events: #{sm.events.keys.join(', ')})"
166
195
  event_obj.branches.each do |branch|
167
196
  branch.state_requirements.each do |state_req|
168
197
  next unless (from = state_req[:from])
@@ -172,13 +201,25 @@ module Sequel
172
201
  return false
173
202
  end
174
203
 
175
- def validates_state_machine
176
- states = self.class.state_machine.states.map(&:value)
177
- state = self.state_machine_status
204
+ def validates_state_machine(machine: nil)
205
+ state_machine = find_state_machine(machine)
206
+ states = state_machine.states.map(&:value)
207
+ state = self.sequel_state_machine_status(state_machine.attribute)
178
208
  return if states.include?(state)
179
- self.errors.add(self.state_machine_status_column,
209
+ self.errors.add(self.state_machine_status_column(machine),
180
210
  "state '#{state}' must be one of (#{states.sort.join(', ')})",)
181
211
  end
212
+
213
+ private def find_state_machine(machine)
214
+ machines = self.class.state_machines
215
+ raise InvalidConfiguration, "no state machines defined" if machines.empty?
216
+ if machine
217
+ m = machines[machine]
218
+ raise ArgumentError, "no state machine named #{machine}" if m.nil?
219
+ return m
220
+ end
221
+ return machines.first[1]
222
+ end
182
223
  end
183
224
 
184
225
  module ClassMethods
@@ -6,26 +6,39 @@ require "sequel/model"
6
6
  module Sequel
7
7
  module Plugins
8
8
  module StateMachineAuditLog
9
+ DEFAULT_COLUMN_MAPPINGS = {
10
+ at: :at,
11
+ event: :event,
12
+ to_state: :to_state,
13
+ from_state: :from_state,
14
+ reason: :reason,
15
+ messages: :messages,
16
+ actor_id: :actor_id,
17
+ actor: :actor,
18
+ machine_name: :machine_name,
19
+ }.freeze
9
20
  DEFAULT_OPTIONS = {
10
21
  messages_supports_array: :undefined,
11
- column_mappings: {
12
- at: :at,
13
- event: :event,
14
- to_state: :to_state,
15
- from_state: :from_state,
16
- reason: :reason,
17
- messages: :messages,
18
- actor_id: :actor_id,
19
- }.freeze,
22
+ column_mappings: DEFAULT_COLUMN_MAPPINGS,
20
23
  }.freeze
21
24
  def self.configure(model, opts=DEFAULT_OPTIONS)
22
25
  opts = DEFAULT_OPTIONS.merge(opts)
23
- model.state_machine_column_mappings = opts[:column_mappings]
26
+ colmap = opts[:column_mappings]
27
+ actor_key_mismatch = (colmap.key?(:actor) && !colmap.key?(:actor_id)) ||
28
+ (!colmap.key?(:actor) && colmap.key?(:actor_id))
29
+ if actor_key_mismatch
30
+ msg = "Remapping columns :actor and :actor_id must both be supplied"
31
+ raise Sequel::Plugins::StateMachine::InvalidConfiguration, msg
32
+ end
33
+ colmap = DEFAULT_COLUMN_MAPPINGS.merge(colmap)
34
+ model.state_machine_column_mappings = colmap
24
35
  msgarray = opts[:messages_supports_array] || true
25
36
  if msgarray == :undefined
26
37
  msgcol = model.state_machine_column_mappings[:messages]
27
- dbt = model.db_schema[msgcol][:db_type]
28
- msgarray = dbt.include?("json") || dbt.include?("[]")
38
+ if model.db_schema && model.db_schema[msgcol]
39
+ dbt = model.db_schema[msgcol][:db_type]
40
+ msgarray = dbt.include?("json") || dbt.include?("[]")
41
+ end
29
42
  end
30
43
  model.state_machine_messages_supports_array = msgarray
31
44
  end
@@ -52,8 +65,8 @@ module Sequel
52
65
 
53
66
  module InstanceMethods
54
67
  def failed?
55
- from_state = self._get_mapped_column_value(:from_state)
56
- to_state = self._get_mapped_column_value(:to_state)
68
+ from_state = self.sequel_state_machine_get(:from_state)
69
+ to_state = self.sequel_state_machine_get(:to_state)
57
70
  return from_state == to_state
58
71
  end
59
72
 
@@ -62,12 +75,25 @@ module Sequel
62
75
  end
63
76
 
64
77
  def full_message
65
- msg = self._get_mapped_column_value(:messages)
78
+ msg = self.sequel_state_machine_get(:messages)
66
79
  return self.class.state_machine_messages_supports_array ? msg.join(", ") : msg
67
80
  end
68
81
 
69
- def _get_mapped_column_value(col)
70
- return self[self.class.state_machine_column_mappings[col]]
82
+ def sequel_state_machine_get(unmapped)
83
+ return self[self.class.state_machine_column_mappings[unmapped]]
84
+ end
85
+
86
+ def sequel_state_machine_set(unmapped, value)
87
+ self[self.class.state_machine_column_mappings[unmapped]] = value
88
+ end
89
+
90
+ def sequel_state_machine_map_columns(**kw)
91
+ # We may pass in a machine_name of nil when we are using single-state-machine models.
92
+ # In this case, we assume the audit logs don't have a machine_name column,
93
+ # so always remove the column if the machine is nil.
94
+ kw.delete(:machine_name) if kw[:machine_name].nil?
95
+ mappings = self.class.state_machine_column_mappings
96
+ return kw.transform_keys { |k| mappings[k] or raise KeyError, "field #{k} unmapped in #{mappings}" }
71
97
  end
72
98
  end
73
99
  end
@@ -4,9 +4,15 @@ require "rspec"
4
4
 
5
5
  RSpec::Matchers.define :transition_on do |event|
6
6
  match do |receiver|
7
- raise 'must provide a "to" state' if (@to || "").to_s.empty?
7
+ raise ArgumentError, "must use :to to provide a target state" if (@to || "").to_s.empty?
8
+ raise ArgumentError, "must use :of_machine for use with multiple state machines" if
9
+ @machine.nil? && receiver.class.state_machines.length > 1
8
10
  receiver.send(event, *@args)
9
- @to == receiver.send(:state_machine_status)
11
+ @to == receiver.send(@machine || :sequel_state_machine_status)
12
+ end
13
+
14
+ chain :of_machine do |col|
15
+ @machine = col
10
16
  end
11
17
 
12
18
  chain :to do |to_state|
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StateMachines
4
4
  module Sequel
5
- VERSION = "1.1.0"
5
+ VERSION = "1.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel-state-machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lithic Tech