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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f0c0bd5c716d726e9e8ba679d545038637a3ed7da8a569918e24725d0558021
|
4
|
+
data.tar.gz: dcba20e8cb5eb3fbcbad3e70e5e5d0662d3515c44ebb6fa033efc5ae4f1e7a02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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 =
|
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 = "
|
42
|
-
|
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
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
-
|
67
|
-
|
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
|
83
|
+
return alog
|
70
84
|
end
|
71
85
|
|
72
86
|
def commit_audit_log(transition)
|
73
|
-
|
74
|
-
|
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
|
-
@
|
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.
|
136
|
+
mapped_values = audlog.sequel_state_machine_map_columns(
|
119
137
|
at: Time.now,
|
120
138
|
event: event,
|
121
|
-
from_state: self.
|
122
|
-
to_state: self.
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
177
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
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.
|
56
|
-
to_state = self.
|
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.
|
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
|
70
|
-
return self[self.class.state_machine_column_mappings[
|
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
|
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(:
|
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|
|