Module: Sequel::Plugins::StateMachine::InstanceMethods

Defined in:
lib/sequel/plugins/state_machine.rb

Instance Method Summary collapse

Instance Method Details

#audit(message, reason: nil, machine: nil) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/sequel/plugins/state_machine.rb', line 131

def audit(message, reason: nil, machine: nil)
  audlog = self.current_audit_log(machine: machine)
  if audlog.class.state_machine_messages_supports_array
    audlog.messages ||= []
    audlog.messages << message
  else
    audlog.messages ||= ""
    audlog.messages += (audlog.messages.empty? ? message : ("\n" + message))
  end
  audlog.reason = reason if reason
  return audlog
end

#audit_logs_for(machine) ⇒ Object

Return audit logs for the given state machine name. Only useful for multi-state-machine models.



163
164
165
166
# File 'lib/sequel/plugins/state_machine.rb', line 163

def audit_logs_for(machine)
  lines = self.send(self.class.instance_variable_get(:@sequel_state_machine_audit_logs_association))
  return lines.select { |ln| ln.sequel_state_machine_get(:machine_name) == machine.to_s }
end

#audit_one_off(event, messages, reason: nil, machine: nil) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/sequel/plugins/state_machine.rb', line 144

def audit_one_off(event, messages, reason: nil, machine: nil)
  messages = [messages] unless messages.respond_to?(:to_ary)
  audlog = self.new_audit_log
  mapped_values = audlog.sequel_state_machine_map_columns(
    at: Time.now,
    event: event,
    from_state: self.sequel_state_machine_status(machine),
    to_state: self.sequel_state_machine_status(machine),
    messages: audlog.class.state_machine_messages_supports_array ? messages : messages.join("\n"),
    reason: reason || "",
    actor: StateMachines::Sequel.current_actor,
    machine_name: machine,
  )
  audlog.set(mapped_values)
  return self.add_audit_log(audlog)
end

#commit_audit_log(transition) ⇒ Object

Commit pending changes to the audit log. This involves either:

  • Updating the last audit log step, if it matches our current criteria (event, from state, to state),

  • or creating a new audit log entry.

This ensures that we have the following behavior:

  • Failed transitions - ie where from and to state are the same - do not add multiple audit log steps. Only the latest failed transition is recorded.

  • Successful transitions are always recorded. If we transition, a->b->c, and then reset the state machine to ‘a’ and transition a->b->c again, we’d end up with 5 transitions (a->b->c->a->b->c).



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/sequel/plugins/state_machine.rb', line 96

def commit_audit_log(transition)
  machine = self.class.state_machines.length > 1 ? transition.machine.name : nil
  StateMachines::Sequel.log(
    self, :debug, "committing_audit_log", {transition: transition, state_machine: machine},
  )
  current = self.current_audit_log(machine: machine)

  last_log = self.audit_logs.last
  can_update_last = last_log &&
    last_log.sequel_state_machine_get(:event) == transition.event.to_s &&
    last_log.sequel_state_machine_get(:from_state) == transition.from &&
    last_log.sequel_state_machine_get(:to_state) == transition.to
  if can_update_last
    StateMachines::Sequel.log(self, :debug, "updating_audit_log", {audit_log_id: last_log.id})
    mapped_values = last_log.sequel_state_machine_map_columns(
      at: Time.now,
      actor: StateMachines::Sequel.current_actor,
      messages: current.messages || (current.class.state_machine_messages_supports_array ? [] : ""),
      reason: current.reason,
    )
    last_log.update(**mapped_values)
  else
    StateMachines::Sequel.log(self, :debug, "creating_audit_log", {})
    current.set(**current.sequel_state_machine_map_columns(
      at: Time.now,
      actor: StateMachines::Sequel.current_actor,
      event: transition.event.to_s,
      from_state: transition.from,
      to_state: transition.to,
    ))
    self.add_audit_log(current)
  end
  @current_audit_logs[machine] = nil
end

#current_audit_log(machine: nil) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/sequel/plugins/state_machine.rb', line 66

def current_audit_log(machine: nil)
  @current_audit_logs ||= {}
  alog = @current_audit_logs[machine]
  if alog.nil?
    StateMachines::Sequel.log(self, :debug, "preparing_audit_log", {})
    alog = self.new_audit_log
    if machine
      machine_name_col = alog.class.state_machine_column_mappings[:machine_name]
      unless alog.respond_to?(machine_name_col)
        msg = "Audit logs must have a :machine_name field for multi-machine models or if specifying :machine."
        raise InvalidConfiguration, msg
      end
      alog.sequel_state_machine_set(:machine_name, machine)
    end
    @current_audit_logs[machine] = alog
    alog.sequel_state_machine_set(:reason, "")
  end
  return alog
end

#must_process(event, *args) ⇒ Object

Same as process, but raises an error if the transition fails.



181
182
183
184
185
# File 'lib/sequel/plugins/state_machine.rb', line 181

def must_process(event, *args)
  success = self.process(event, *args)
  raise StateMachines::Sequel::FailedTransition.new(self, event) unless success
  return self
end

#new_audit_logObject



54
55
56
57
58
59
60
61
62
63
64
# File 'lib/sequel/plugins/state_machine.rb', line 54

def new_audit_log
  assoc_name = self.class.instance_variable_get(:@sequel_state_machine_audit_logs_association)
  unless (audit_log_assoc = self.class.association_reflections[assoc_name])
    msg = "Association for audit logs '#{assoc_name}' does not exist. " \
          "Your model must have 'one_to_many :audit_logs' for its audit log lines, " \
          "or pass the :audit_logs_association parameter to the plugin to define its association name."
    raise InvalidConfiguration, msg
  end
  audit_log_cls = audit_log_assoc[:class] || Kernel.const_get(audit_log_assoc[:class_name])
  return audit_log_cls.new
end

#process(event, *args) ⇒ Object

Send event with arguments inside of a transaction, save the changes to the receiver, and return the transition result. Used to ensure the event processing happens in a transaction and the receiver is saved.



171
172
173
174
175
176
177
178
# File 'lib/sequel/plugins/state_machine.rb', line 171

def process(event, *args)
  self.db.transaction do
    self.lock!
    result = self.send(event, *args)
    self.save_changes
    return result
  end
end

#process_if(event, *args) ⇒ Object

Same as must_process, but takes a lock, and calls the given block, only doing actual processing if the block returns true. If the block returns false, it acts as a success. Used to avoid issues concurrently processing the same object through the same state.



191
192
193
194
195
196
197
# File 'lib/sequel/plugins/state_machine.rb', line 191

def process_if(event, *args)
  self.db.transaction do
    self.lock!
    return self unless yield(self)
    return self.must_process(event, *args)
  end
end

#sequel_state_machine_status(machine = nil) ⇒ Object



50
51
52
# File 'lib/sequel/plugins/state_machine.rb', line 50

def sequel_state_machine_status(machine=nil)
  return self.send(self.state_machine_status_column(machine))
end

#valid_state_path_through?(event, machine: nil) ⇒ Boolean

Return true if the given event can be transitioned into by the current state.

Returns:

  • (Boolean)


200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/sequel/plugins/state_machine.rb', line 200

def valid_state_path_through?(event, machine: nil)
  current_state_str = self.sequel_state_machine_status(machine).to_s
  current_state_sym = current_state_str.to_sym
  sm = find_state_machine(machine)
  event_obj = sm.events[event] or
    raise ArgumentError, "Invalid event #{event} (available #{sm.name} events: #{sm.events.keys.join(', ')})"
  event_obj.branches.each do |branch|
    branch.state_requirements.each do |state_req|
      next unless (from = state_req[:from])
      return true if from.matches?(current_state_str) || from.matches?(current_state_sym)
    end
  end
  return false
end

#validates_state_machine(machine: nil) ⇒ Object



215
216
217
218
219
220
221
222
# File 'lib/sequel/plugins/state_machine.rb', line 215

def validates_state_machine(machine: nil)
  state_machine = find_state_machine(machine)
  states = state_machine.states.map(&:value)
  state = self.sequel_state_machine_status(state_machine.attribute)
  return if states.include?(state)
  self.errors.add(self.state_machine_status_column(machine),
                  "state '#{state}' must be one of (#{states.sort.join(', ')})",)
end