lib/process_executer/destinations.rb



# frozen_string_literal: true

require_relative 'destinations/child_redirection'
require_relative 'destinations/file_descriptor'
require_relative 'destinations/file_path'
require_relative 'destinations/file_path_mode'
require_relative 'destinations/file_path_mode_perms'
require_relative 'destinations/io'
require_relative 'destinations/monitored_pipe'
require_relative 'destinations/stderr'
require_relative 'destinations/stdout'
require_relative 'destinations/tee'
require_relative 'destinations/writer'

module ProcessExecuter
  # Collection of destination handler implementations
  #
  # @api public
  module Destinations
    # Creates appropriate destination objects based on the given destination
    #
    # This factory method dynamically finds and instantiates the appropriate
    # destination class for handling the provided destination.
    #
    # @param destination [Object] the destination to create a handler for
    # @return [DestinationBase] an instance of the appropriate destination handler
    # @raise [ArgumentError] if no matching destination class is found
    #
    # @example
    #   ProcessExecuter.destination_factory(1) #=> Returns a Stdout instance
    #   ProcessExecuter.destination_factory("output.log") #=> Returns a FilePath instance
    def self.factory(destination)
      matching_class = matching_destination_class(destination)
      return matching_class.new(destination) if matching_class

      raise ArgumentError, 'wrong exec redirect action'
    end

    # Determines if the given destination is compatible with a monitored pipe
    #
    # If true, this destination should not be wrapped in a monitored pipe.
    #
    # @example
    #   ProcessExecuter::Destinations.compatible_with_monitored_pipe?(1) #=> true
    #   ProcessExecuter::Destinations.compatible_with_monitored_pipe?([:child, 6]) #=> false
    #   ProcessExecuter::Destinations.compatible_with_monitored_pipe?([:close]) #=> false
    #
    # @param destination [Object] the destination to check
    # @return [Boolean, nil] true if the destination is compatible with a monitored pipe
    # @raise [ArgumentError] if no matching destination class is found
    # @api public
    def self.compatible_with_monitored_pipe?(destination)
      matching_class = matching_destination_class(destination)
      matching_class&.compatible_with_monitored_pipe?
    end

    # Determines the destination class that can handle the given destination
    # @param destination [Object] the destination to check
    # @return [Class] the destination class that can handle the given destination
    # @api private
    def self.matching_destination_class(destination)
      destination_classes =
        ProcessExecuter::Destinations.constants
                                     .map { |const| ProcessExecuter::Destinations.const_get(const) }
                                     .select { |const| const.is_a?(Class) }

      destination_classes.find { |klass| klass.handles?(destination) }
    end
  end
end