sequel-state-machine 1.1.0 → 1.2.2
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.
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
|