module StateGate::Builder::TransitionValidationMethods

Description

Multiple private methods allowing StateGate::Builder to generate attribute setter methods for transition validation.

| Forcing a change

To force a status change that would otherwise be prohibited, preceed the new state with force_ :

.status = :archived         # => ArgumentError
.status = :force_archived   # => :archived

Private Instance Methods

_prepend__attribute_equals() click to toggle source

Adds a method to overwrite the attribute :<attr>=(val) setter, raising an error if the supplied value is not a valid transition

eg:
    .status = :archived  # => ArgumentError
    .status - :active    # => :active

actions

+ assert it’s a valid transition + call super

# File lib/state_gate/builder/transition_validation_methods.rb, line 139
def _prepend__attribute_equals
  attr_name = @attribute

  _transition_validation_module.module_eval(%(
    def #{attr_name}=(new_val)
      stateables[StateGate.symbolize(:#{attr_name})] \
          &.assert_valid_transition!(self[:#{attr_name}], new_val)
      super(new_val)
    end
  ), __FILE__, __LINE__ - 6)
end
_prepend__initialize() click to toggle source

Prepends an :itialize method to ensure the attribute is not set on initializing a new instance unless :forced.

Klass.new(status: :archived)  # => ArgumentError
# File lib/state_gate/builder/transition_validation_methods.rb, line 223
def _prepend__initialize # rubocop:disable Metrics/MethodLength
  return if _transition_validation_module.method_defined?(:initialize)

  _transition_validation_module.module_eval(%(
    def initialize(attributes = nil, &block)
      attributes&.each do |attr_name, value|
        key = self.class.attribute_aliases[attr_name.to_s] || attr_name
        if self.stateables.keys.include?(key.to_sym)
          unless value.to_s.start_with?('force_')
            msg = ":\#{attr_name} may not be included in the parameters for a new" \
            " \#{self.class.name}.  Create the new instance first, then transition" \
            " :\#{attr_name} as required."
            fail ArgumentError, msg
          end
        end
      end

      super
    end
  ), __FILE__, __LINE__ - 13)
end
_prepend__update_columns() click to toggle source

Adds a method to overwrite the instance :update_columns(attr: val) setter, raising an error if the supplied value is not a valid transition

eg:
    .update_columns(status: :archived)  # => ArgumentError
    .update_columns(status: :active)    # => :active

actions

+ loop through each attribute + get the base attribute name from any alias used + assert it’s a valid transition + call super

# File lib/state_gate/builder/transition_validation_methods.rb, line 196
def _prepend__update_columns # rubocop:disable Metrics/MethodLength
  return if _transition_validation_module.method_defined?(:update_columns)

  _transition_validation_module.module_eval(%(
    def update_columns(args)
      super(args) and return if (new_record? || destroyed?)

      args.each do |key, value|
        name = key.to_s.downcase
        name = self.class.attribute_aliases[name] || name

        stateables[StateGate.symbolize(name)] \
            &.assert_valid_transition!(self[name], value)
      end

      super
    end
  ), __FILE__, __LINE__ - 14)
end
_prepend__write_attribute() click to toggle source

Adds a method to overwrite the instance :write_attribute(attr, val) setter, raising an error if the supplied value is not a valid transition

eg:
    .write_attribute(:status, :archived)  # => ArgumentError
    .write_attribute(:status, :active)    # => :active

actions

+ loop through each attribute + get the base attribute name from any alias used + assert it’s a valid transition + call super

# File lib/state_gate/builder/transition_validation_methods.rb, line 166
def _prepend__write_attribute
  return if _transition_validation_module.method_defined?(:write_attribute)

  _transition_validation_module.module_eval(%(
    def write_attribute(attrribute_name, new_val = nil)
      name = attrribute_name.to_s.downcase
      name = self.class.attribute_aliases[name] || name

      stateables[StateGate.symbolize(name)] \
          &.assert_valid_transition!(self[name], new_val)
      super(attrribute_name, new_val)
    end
  ), __FILE__, __LINE__ - 9)
end
_transition_validation_module() click to toggle source

Dynamically generated module to hold the validation setter methods and is pre-pended to the class.

A new module is create if it doesn’t already exist.

Note:

the module is named "StateGate::<klass>TranstionValidationMethods"
# File lib/state_gate/builder/transition_validation_methods.rb, line 107
def _transition_validation_module # rubocop:disable Metrics/MethodLength
  @_transition_validation_module ||= begin
    mod_name = 'StateGate_ValidationMethods'

    if @klass.const_defined?(mod_name)
      "#{@klass}::#{mod_name}".constantize
    else
      @klass.const_set(mod_name, Module.new)
      mod = "#{@klass}::#{mod_name}".constantize
      @klass.prepend mod
      mod
    end
  end
end
generate_transition_validation_methods() click to toggle source

Add prepended instance methods to the klass that catch all methods for updating the attribute and validated the new value is an allowed transition

Note:

These methods are only added if the engine has an
include_transition_validations? status on initialisation

Note:

The three methods "<atrr>=(val)", "write_attribute(<attr>, val)" and
"update_columns(<attr>: val)" cover all the possibilities of setting the
attribute through ActiveRecord.
# File lib/state_gate/builder/transition_validation_methods.rb, line 84
def generate_transition_validation_methods
  return if @engine.transitionless?

  _prepend__attribute_equals
  _prepend__write_attribute
  _prepend__update_columns
  _prepend__initialize
end