class StateMachine::YARD::Handlers::Machine

Handles and processes state_machine

Attributes

machine[R]

The generated state machine

Public Instance Methods

process() click to toggle source
# File lib/state_machines/yard/handlers/machine.rb, line 12
def process
  # Cross-file storage for state machines
  globals.state_machines ||= Hash.new { |h, k| h[k] = {} }
  namespace['state_machines'] ||= {}

  # Create new machine
  klass = inherited_machine ? Class.new(inherited_machine.owner_class) : Class.new { extend StateMachines::MacroMethods }
  @machine = klass.state_machine(name, options) {}

  # Track the state machine
  globals.state_machines[namespace.name][name] = machine
  namespace['state_machines'][name] = {name: name, description: statement.docstring}

  # Parse the block
  parse_block(statement.last.last, owner: machine)

  # Draw the machine for reference in the template
  file = Tempfile.new(['state_machine', '.png'])
  begin
    if machine.draw(name: File.basename(file.path, '.png'), path: File.dirname(file.path), orientation: 'landscape')
      namespace['state_machines'][name][:image] = file.read
    end
  ensure
    # Clean up tempfile
    file.close
    file.unlink
  end

  # Define auto-generated methods
  define_macro_methods
  define_state_methods
  define_event_methods
end

Protected Instance Methods

define_event_methods() click to toggle source

Defines auto-generated event methods for the given machine

# File lib/state_machines/yard/handlers/machine.rb, line 277
def define_event_methods
  machine.events.each do |event|
    next if inherited_machine && inherited_machine.events[event.name]

    # Event query
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "can_#{event.qualified_name}?"))
    m.docstring = [
        "Checks whether #{event.name.inspect} can be fired.",
        '@param [Hash] requirements The transition requirements to test against',
        "@option requirements [#{state_type}] :from (the current state) One or more initial states",
        "@option requirements [#{state_type}] :to One or more target states",
        '@option requirements [Boolean] :guard Whether to guard transitions with conditionals',
        "@return [Boolean] +true+ if #{event.name.inspect} can be fired, otherwise +false+"
    ]
    m.parameters = [['requirements', '{}']]

    # Event transition
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "#{event.qualified_name}_transition"))
    m.docstring = [
        "Gets the next transition that would be performed if #{event.name.inspect} were to be fired.",
        '@param [Hash] requirements The transition requirements to test against',
        "@option requirements [#{state_type}] :from (the current state) One or more initial states",
        "@option requirements [#{state_type}] :to One or more target states",
        '@option requirements [Boolean] :guard Whether to guard transitions with conditionals',
        '@return [StateMachines::Transition] The transition that would be performed or +nil+'
    ]
    m.parameters = [['requirements', '{}']]

    # Fire event
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, event.qualified_name))
    m.docstring = [
        "Fires the #{event.name.inspect} event.",
        '@param [Array] args Optional arguments to include in transition callbacks',
        '@return [Boolean] +true+ if the transition succeeds, otherwise +false+'
    ]
    m.parameters = ['*args']

    # Fire event (raises exception)
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "#{event.qualified_name}!"))
    m.docstring = [
        "Fires the #{event.name.inspect} event, raising an exception if it fails.",
        '@param [Array] args Optional arguments to include in transition callbacks',
        '@return [Boolean] +true+ if the transition succeeds',
        '@raise [StateMachines::InvalidTransition] If the transition fails'
    ]
    m.parameters = ['*args']
  end
end
define_macro_methods() click to toggle source

Defines auto-generated macro methods for the given machine

# File lib/state_machines/yard/handlers/machine.rb, line 134
def define_macro_methods
  return if inherited_machine

  # Human state name lookup
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "human_#{machine.attribute(:name)}", :class))
  m.docstring = [
      'Gets the humanized name for the given state.',
      "@param [#{state_type}] state The state to look up",
      '@return [String] The human state name'
  ]
  m.parameters = ['state']

  # Human event name lookup
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "human_#{machine.attribute(:event_name)}", :class))
  m.docstring = [
      'Gets the humanized name for the given event.',
      "@param [#{event_type}] event The event to look up",
      '@return [String] The human event name'
  ]
  m.parameters = ['event']

  # Only register attributes when the accessor isn't explicitly defined
  # by the class / superclass *and* isn't defined by inference from the
  # ORM being used
  unless integration || instance_attributes.include?(machine.attribute.to_sym)
    attribute = machine.attribute
    namespace.attributes[:instance][attribute] = {}

    # Machine attribute getter
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, attribute))
    namespace.attributes[:instance][attribute][:read] = m
    m.docstring = [
        'Gets the current attribute value for the machine',
        '@return The attribute value'
    ]

    # Machine attribute setter
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "#{attribute}="))
    namespace.attributes[:instance][attribute][:write] = m
    m.docstring = [
        'Sets the current value for the machine',
        "@param new_#{attribute} The new value to set"
    ]
    m.parameters = ["new_#{attribute}"]
  end

  if integration && integration.defaults[:action] && !options.include?(:action) || options[:action]
    attribute = "#{machine.name}_event"
    namespace.attributes[:instance][attribute] = {}

    # Machine event attribute getter
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, attribute))
    namespace.attributes[:instance][attribute][:read] = m
    m.docstring = [
        'Gets the current event attribute value for the machine',
        '@return The event attribute value'
    ]

    # Machine event attribute setter
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "#{attribute}="))
    namespace.attributes[:instance][attribute][:write] = m
    m.docstring = [
        'Sets the current value for the machine',
        "@param new_#{attribute} The new value to set"
    ]
    m.parameters = ["new_#{attribute}"]
  end

  # Presence query
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "#{machine.name}?"))
  m.docstring = [
      'Checks the given state name against the current state.',
      "@param [#{state_type}] state_name The name of the state to check",
      '@return [Boolean] True if they are the same state, otherwise false',
      '@raise [IndexError] If the state name is invalid'
  ]
  m.parameters = ['state_name']

  # Internal state name
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, machine.attribute(:name)))
  m.docstring = [
      'Gets the internal name of the state for the current value.',
      "@return [#{state_type}] The internal name of the state"
  ]

  # Human state name
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "human_#{machine.attribute(:name)}"))
  m.docstring = [
      'Gets the human-readable name of the state for the current value.',
      '@return [String] The human-readable state name'
  ]

  # Available events
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, machine.attribute(:events)))
  m.docstring = [
      "Gets the list of events that can be fired on the current #{machine.name} (uses the *unqualified* event names)",
      '@param [Hash] requirements The transition requirements to test against',
      "@option requirements [#{state_type}] :from (the current state) One or more initial states",
      "@option requirements [#{state_type}] :to One or more target states",
      "@option requirements [#{event_type}] :on One or more events that fire the transition",
      '@option requirements [Boolean] :guard Whether to guard transitions with conditionals',
      "@return [Array<#{event_type}>] The list of event names"
  ]
  m.parameters = [['requirements', '{}']]

  # Available transitions
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, machine.attribute(:transitions)))
  m.docstring = [
      "Gets the list of transitions that can be made for the current #{machine.name}",
      '@param [Hash] requirements The transition requirements to test against',
      "@option requirements [#{state_type}] :from (the current state) One or more initial states",
      "@option requirements [#{state_type}] :to One or more target states",
      "@option requirements [#{event_type}] :on One or more events that fire the transition",
      '@option requirements [Boolean] :guard Whether to guard transitions with conditionals',
      '@return [Array<StateMachines::Transition>] The available transitions'
  ]
  m.parameters = [['requirements', '{}']]

  # Available transition paths
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, machine.attribute(:paths)))
  m.docstring = [
      "Gets the list of sequences of transitions that can be run for the current #{machine.name}",
      '@param [Hash] requirements The transition requirements to test against',
      "@option requirements [#{state_type}] :from (the current state) The initial state",
      "@option requirements [#{state_type}] :to The target state",
      '@option requirements [Boolean] :deep Whether to enable deep searches for the target state',
      '@option requirements [Boolean] :guard Whether to guard transitions with conditionals',
      '@return [StateMachines::PathCollection] The collection of paths'
  ]
  m.parameters = [['requirements', '{}']]

  # Generic event fire
  register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "fire_#{machine.attribute(:event)}"))
  m.docstring = [
      "Fires an arbitrary #{machine.name} event with the given argument list",
      "@param [#{event_type}] event The name of the event to fire",
      '@param args Optional arguments to include in the transition',
      '@return [Boolean] +true+ if the event succeeds, otherwise +false+'
  ]
  m.parameters = ['event', '*args']
end
define_state_methods() click to toggle source

Defines auto-generated state methods for the given machine

# File lib/state_machines/yard/handlers/machine.rb, line 327
def define_state_methods
  machine.states.each do |state|
    next if inherited_machine && inherited_machine.states[state.name] || !state.name

    # State query
    register(m = ::YARD::CodeObjects::MethodObject.new(namespace, "#{state.qualified_name}?"))
    m.docstring = [
        "Checks whether #{state.name.inspect} is the current state.",
        '@return [Boolean] +true+ if this is the current state, otherwise +false+'
    ]
  end
end
event_type() click to toggle source

Gets the class type being used to define events. Default is “Symbol”.

# File lib/state_machines/yard/handlers/machine.rb, line 129
def event_type
  @event_type ||= machine.events.any? ? machine.events.first.name.class.to_s : 'Symbol'
end
inherited_machine() click to toggle source

Gets the machine that was inherited from a superclass. This also ensures each ancestor has been loaded prior to looking up their definitions.

# File lib/state_machines/yard/handlers/machine.rb, line 83
def inherited_machine
  @inherited_machine ||= begin
    namespace.inheritance_tree.each do |ancestor|
      begin
        ensure_loaded!(ancestor)
      rescue ::YARD::Handlers::NamespaceMissingError
        # Ignore: just means that we can't access an ancestor
      end
    end

    # Find the first ancestor that has the machine
    loaded_superclasses.find do |superclass|
      if superclass != namespace
        machine = globals.state_machines[superclass.name][name]
        break machine if machine
      end
    end
  end
end
instance_attributes() click to toggle source

Gets a list of all attributes for the current class, including those that are inherited

# File lib/state_machines/yard/handlers/machine.rb, line 111
def instance_attributes
  attributes = {}
  loaded_superclasses.each { |superclass| attributes.merge!(superclass.instance_attributes) }
  attributes
end
integration() click to toggle source

Gets the type of ORM integration being used based on the list of ancestors (including mixins)

# File lib/state_machines/yard/handlers/machine.rb, line 119
def integration
  @integration ||= Integrations.match_ancestors(namespace.inheritance_tree(true).map { |ancestor| ancestor.path })
end
loaded_superclasses() click to toggle source

Gets members of this class's superclasses have already been loaded by YARD

# File lib/state_machines/yard/handlers/machine.rb, line 105
def loaded_superclasses
  namespace.inheritance_tree.select { |ancestor| ancestor.is_a?(::YARD::CodeObjects::ClassObject) }
end
name() click to toggle source

Extracts the machine name's

# File lib/state_machines/yard/handlers/machine.rb, line 48
def name
  @name ||= begin
    ast = statement.parameters.first
    if ast && [:symbol_literal, :string_literal].include?(ast.type)
      extract_node_name(ast)
    else
      :state
    end
  end
end
options() click to toggle source

Extracts the machine options. Note that this will only extract a subset of the options supported.

# File lib/state_machines/yard/handlers/machine.rb, line 61
def options
  @options ||= begin
    options = {}
    ast = statement.parameters(false).last

    if !inherited_machine && ast && ![:symbol_literal, :string_literal].include?(ast.type)
      ast.children.each do |assoc|
        # Only extract important options
        key = extract_node_name(assoc[0])
        next unless [:initial, :attribute, :namespace, :action].include?(key)

        value = extract_node_name(assoc[1])
        options[key] = value
      end
    end

    options
  end
end
state_type() click to toggle source

Gets the class type being used to define states. Default is “Symbol”.

# File lib/state_machines/yard/handlers/machine.rb, line 124
def state_type
  @state_type ||= machine.states.any? ? machine.states.map { |state| state.name }.compact.first.class.to_s : 'Symbol'
end