class Tapioca::Compilers::Dsl::StateMachines

`Tapioca::Compilers::Dsl::StateMachines` generates RBI files for classes that setup a [`state_machine`](github.com/state-machines/state_machines). The generator also processes the extra methods generated by [StateMachines Active Record](github.com/state-machines/state_machines-activerecord) and [StateMachines Active Model](github.com/state-machines/state_machines-activemodel) integrations.

For example, with the following `Vehicle` class:

~~~rb class Vehicle

state_machine :alarm_state, initial: :active, namespace: :'alarm' do
  event :enable do
    transition all => :active
  end

  event :disable do
    transition all => :off
  end

  state :active, :value => 1
  state :off, :value => 0
end

end ~~~

this generator will produce the RBI file `vehicle.rbi` with the following content:

~~~rbi # vehicle.rbi # typed: true class Vehicle

include StateMachineInstanceHelperModule
extend StateMachineClassHelperModule

module StateMachineClassHelperModule
  sig { params(event: T.any(String, Symbol)).returns(String) }
  def human_alarm_state_event_name(event); end

  sig { params(state: T.any(String, Symbol)).returns(String) }
  def human_alarm_state_name(state); end
end

module StateMachineInstanceHelperModule
  sig { returns(T::Boolean) }
  def alarm_active?; end

  sig { returns(T::Boolean) }
  def alarm_off?; end

  sig { returns(Integer) }
  def alarm_state; end

  sig { params(value: Integer).returns(Integer) }
  def alarm_state=(value); end

  sig { params(state: T.any(String, Symbol)).returns(T::Boolean) }
  def alarm_state?(state); end

  sig { params(args: T.untyped).returns(T::Array[T.any(String, Symbol)]) }
  def alarm_state_events(*args); end

  sig { returns(T.any(String, Symbol)) }
  def alarm_state_name; end

  sig { params(args: T.untyped).returns(T::Array[::StateMachines::Transition]) }
  def alarm_state_paths(*args); end

  sig { params(args: T.untyped).returns(T::Array[::StateMachines::Transition]) }
  def alarm_state_transitions(*args); end

  sig { returns(T::Boolean) }
  def can_disable_alarm?; end

  sig { returns(T::Boolean) }
  def can_enable_alarm?; end

  sig { params(args: T.untyped).returns(T::Boolean) }
  def disable_alarm(*args); end

  sig { params(args: T.untyped).returns(T::Boolean) }
  def disable_alarm!(*args); end

  sig { params(args: T.untyped).returns(T.nilable(::StateMachines::Transition)) }
  def disable_alarm_transition(*args); end

  sig { params(args: T.untyped).returns(T::Boolean) }
  def enable_alarm(*args); end

  sig { params(args: T.untyped).returns(T::Boolean) }
  def enable_alarm!(*args); end

  sig { params(args: T.untyped).returns(T.nilable(::StateMachines::Transition)) }
  def enable_alarm_transition(*args); end

  sig { params(event: T.any(String, Symbol), args: T.untyped).returns(T::Boolean) }
  def fire_alarm_state_event(event, *args); end

  sig { returns(String) }
  def human_alarm_state_name; end
end

end ~~~

Public Instance Methods

decorate(root, constant) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 122
def decorate(root, constant)
  return if constant.state_machines.empty?

  root.create_path(T.unsafe(constant)) do |klass|
    instance_module_name = "StateMachineInstanceHelperModule"
    class_module_name = "StateMachineClassHelperModule"

    instance_module = RBI::Module.new(instance_module_name)
    klass << instance_module

    class_module = RBI::Module.new(class_module_name)
    klass << class_module

    constant.state_machines.each_value do |machine|
      state_type = state_type_for(machine)

      define_state_accessor(instance_module, machine, state_type)
      define_state_predicate(instance_module, machine)
      define_event_helpers(instance_module, machine)
      define_path_helpers(instance_module, machine)
      define_name_helpers(instance_module, class_module, machine)
      define_scopes(class_module, machine)

      define_state_methods(instance_module, machine)
      define_event_methods(instance_module, machine)
    end

    matching_integration_name = ::StateMachines::Integrations.match(constant)&.integration_name

    case matching_integration_name
    when :active_record
      define_activerecord_methods(instance_module)
    end

    klass.create_include(instance_module_name)
    klass.create_extend(class_module_name)
  end
end
gather_constants() click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 162
def gather_constants
  all_classes.select { |mod| mod < ::StateMachines::InstanceMethods }
end

Private Instance Methods

define_activerecord_methods(instance_module) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 180
def define_activerecord_methods(instance_module)
  instance_module.create_method(
    "changed_for_autosave?",
    return_type: "T::Boolean"
  )
end
define_event_helpers(instance_module, machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 252
def define_event_helpers(instance_module, machine)
  events_attribute = machine.attribute(:events).to_s
  transitions_attribute = machine.attribute(:transitions).to_s
  event_attribute = machine.attribute(:event).to_s
  event_transition_attribute = machine.attribute(:event_transition).to_s

  instance_module.create_method(
    events_attribute,
    parameters: [create_rest_param("args", type: "T.untyped")],
    return_type: "T::Array[T.any(String, Symbol)]"
  )
  instance_module.create_method(
    transitions_attribute,
    parameters: [create_rest_param("args", type: "T.untyped")],
    return_type: "T::Array[::StateMachines::Transition]"
  )
  instance_module.create_method(
    "fire_#{event_attribute}",
    parameters: [
      create_param("event", type: "T.any(String, Symbol)"),
      create_rest_param("args", type: "T.untyped"),
    ],
    return_type: "T::Boolean"
  )
  if machine.action
    instance_module.create_method(
      event_attribute,
      return_type: "T.nilable(Symbol)"
    )
    instance_module.create_method(
      "#{event_attribute}=",
      parameters: [create_param("value", type: "T.any(String, Symbol)")],
      return_type: "T.any(String, Symbol)"
    )
    instance_module.create_method(
      event_transition_attribute,
      return_type: "T.nilable(::StateMachines::Transition)"
    )
    instance_module.create_method(
      "#{event_transition_attribute}=",
      parameters: [create_param("value", type: "::StateMachines::Transition")],
      return_type: "::StateMachines::Transition"
    )
  end
end
define_event_methods(instance_module, machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 198
def define_event_methods(instance_module, machine)
  machine.events.each do |event|
    instance_module.create_method(
      "can_#{event.qualified_name}?",
      return_type: "T::Boolean"
    )
    instance_module.create_method(
      "#{event.qualified_name}_transition",
      parameters: [create_rest_param("args", type: "T.untyped")],
      return_type: "T.nilable(::StateMachines::Transition)"
    )
    instance_module.create_method(
      event.qualified_name.to_s,
      parameters: [create_rest_param("args", type: "T.untyped")],
      return_type: "T::Boolean"
    )
    instance_module.create_method(
      "#{event.qualified_name}!",
      parameters: [create_rest_param("args", type: "T.untyped")],
      return_type: "T::Boolean"
    )
  end
end
define_name_helpers(instance_module, class_module, machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 316
def define_name_helpers(instance_module, class_module, machine)
  name_attribute = machine.attribute(:name).to_s
  event_name_attribute = machine.attribute(:event_name).to_s

  class_module.create_method(
    "human_#{name_attribute}",
    parameters: [create_param("state", type: "T.any(String, Symbol)")],
    return_type: "String"
  )
  class_module.create_method(
    "human_#{event_name_attribute}",
    parameters: [create_param("event", type: "T.any(String, Symbol)")],
    return_type: "String"
  )
  instance_module.create_method(
    name_attribute,
    return_type: "T.any(String, Symbol)"
  )
  instance_module.create_method(
    "human_#{name_attribute}",
    return_type: "String"
  )
end
define_path_helpers(instance_module, machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 299
def define_path_helpers(instance_module, machine)
  paths_attribute = machine.attribute(:paths).to_s

  instance_module.create_method(
    paths_attribute,
    parameters: [create_rest_param("args", type: "T.untyped")],
    return_type: "T::Array[::StateMachines::Transition]"
  )
end
define_scopes(class_module, machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 341
def define_scopes(class_module, machine)
  helper_modules = machine.instance_variable_get(:@helper_modules)
  class_methods = helper_modules[:class].instance_methods(false)

  class_methods
    .select { |method| method.to_s.start_with?("with_", "without_") }
    .each do |method|
      class_module.create_method(
        method.to_s,
        parameters: [create_rest_param("states", type: "T.any(String, Symbol)")],
        return_type: "T.untyped"
      )
    end
end
define_state_accessor(instance_module, machine, state_type) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 229
def define_state_accessor(instance_module, machine, state_type)
  attribute = machine.attribute.to_s
  instance_module.create_method(
    attribute,
    return_type: state_type
  )
  instance_module.create_method(
    "#{attribute}=",
    parameters: [create_param("value", type: state_type)],
    return_type: state_type
  )
end
define_state_methods(instance_module, machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 188
def define_state_methods(instance_module, machine)
  machine.states.each do |state|
    instance_module.create_method(
      "#{state.qualified_name}?",
      return_type: "T::Boolean"
    )
  end
end
define_state_predicate(instance_module, machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 243
def define_state_predicate(instance_module, machine)
  instance_module.create_method(
    "#{machine.name}?",
    parameters: [create_param("state", type: "T.any(String, Symbol)")],
    return_type: "T::Boolean"
  )
end
state_type_for(machine) click to toggle source
# File lib/tapioca/compilers/dsl/state_machines.rb, line 169
def state_type_for(machine)
  value_types = machine.states.map { |state| state.value.class.name }.uniq

  if value_types.size == 1
    value_types.first
  else
    "T.any(#{value_types.join(", ")})"
  end
end