sequel-state-machine 1.1.0 → 1.2.2
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: 94b067e5d260a2c467a1f1b8b4ec206054c29ab7fc54a86d2b5a89cc7a6620b1
|
4
|
+
data.tar.gz: aea214e6ab04cb78cffc4d580ae1de70011d9f0052562aa3649c217447d9ae0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e8ee131d5e6da8495c7fb998ca81fbfdb43f8a6b7feb52bb919011e51d2d79c38ca190ba12fbf3d866d4aaf0484ae5db7601f903aec162736abdea1f6f05187
|
7
|
+
data.tar.gz: de91701d5ae07856d516c64963cfb988b12333c1e2bcc929f29d37cabad1cc779929cc9d07216fd6af6afb8780e19a2714b40dbb497918b32ca0e1971ce0cd64
|
@@ -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,71 +40,85 @@ 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
|
-
def audit(message, reason: nil)
|
104
|
-
audlog = self.current_audit_log
|
120
|
+
def audit(message, reason: nil, machine: nil)
|
121
|
+
audlog = self.current_audit_log(machine: machine)
|
105
122
|
if audlog.class.state_machine_messages_supports_array
|
106
123
|
audlog.messages ||= []
|
107
124
|
audlog.messages << message
|
@@ -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
|
@@ -2,48 +2,60 @@
|
|
2
2
|
|
3
3
|
require "rspec"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
module StateMachines
|
6
|
+
module Sequel
|
7
|
+
module SpecHelpers
|
8
|
+
module_function def find_state_machine(receiver, event)
|
9
|
+
state_machine = receiver.class.state_machines.values.find { |sm| sm.events[event] }
|
10
|
+
raise ArgumentError, "receiver #{receiver.class} has no state machine for event #{event}" unless state_machine
|
11
|
+
return state_machine
|
12
|
+
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
14
|
+
RSpec::Matchers.define :transition_on do |event|
|
15
|
+
match do |receiver|
|
16
|
+
raise ArgumentError, "must use :to to provide a target state" if (@to || "").to_s.empty?
|
17
|
+
machine = StateMachines::Sequel::SpecHelpers.find_state_machine(receiver, event)
|
18
|
+
receiver.send(event, *@args)
|
19
|
+
current_status = receiver.send(machine.attribute)
|
20
|
+
@to == current_status
|
21
|
+
end
|
15
22
|
|
16
|
-
|
17
|
-
|
18
|
-
|
23
|
+
chain :to do |to_state|
|
24
|
+
@to = to_state
|
25
|
+
end
|
19
26
|
|
20
|
-
|
21
|
-
|
22
|
-
|
27
|
+
chain :with do |*args|
|
28
|
+
@args = args
|
29
|
+
end
|
30
|
+
|
31
|
+
chain :audit do
|
32
|
+
@audit = true
|
33
|
+
end
|
23
34
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
35
|
+
failure_message do |receiver|
|
36
|
+
status = receiver.send(StateMachines::Sequel::SpecHelpers.find_state_machine(receiver, event).attribute)
|
37
|
+
msg =
|
38
|
+
"expected that event #{event} would transition to #{@to} but is #{status}"
|
39
|
+
(msg += "\n#{receiver.audit_logs.map(&:inspect).join("\n")}") if @audit
|
40
|
+
msg
|
41
|
+
end
|
30
42
|
end
|
31
|
-
(msg += "\n#{receiver.audit_logs.map(&:inspect).join("\n")}") if @audit
|
32
|
-
msg
|
33
|
-
end
|
34
|
-
end
|
35
43
|
|
36
|
-
RSpec::Matchers.define :not_transition_on do |event|
|
37
|
-
|
38
|
-
|
39
|
-
|
44
|
+
RSpec::Matchers.define :not_transition_on do |event|
|
45
|
+
match do |receiver|
|
46
|
+
!receiver.send(event, *@args)
|
47
|
+
end
|
40
48
|
|
41
|
-
|
42
|
-
|
43
|
-
|
49
|
+
chain :with do |*args|
|
50
|
+
@args = args
|
51
|
+
end
|
44
52
|
|
45
|
-
|
46
|
-
|
53
|
+
failure_message do |receiver|
|
54
|
+
status = receiver.send(StateMachines::Sequel::SpecHelpers.find_state_machine(receiver, event).attribute)
|
55
|
+
"expected that event #{event} would not transition, but did and is now #{status}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
47
59
|
end
|
48
60
|
end
|
49
61
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sequel-state-machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lithic Tech
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|