szTheory/meta_presenter

View on GitHub
lib/meta_presenter/builder.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'active_support/core_ext/object/try'

module MetaPresenter
  # Builds a presenter class for a controller and method
  class Builder
    # @return [ActionController::Base, ActionMailer::Base] Controller that this presenter will delegate methods to
    attr_reader :controller

    # @return [String] Name of the controller's action to hook the presenter up to
    attr_reader :action_name

    # Creates a new Builder
    #
    # @param [ActionController::Base, ActionMailer::Base] controller Controller that this presenter will delegate methods to
    # @param [String] action_name Name of the controller's action to hook the presenter up to
    def initialize(controller, action_name)
      @controller = controller
      @action_name = action_name
    end

    # Error when there's no presenter class defined but a file for it exists
    class FileExistsButPresenterNotDefinedError < NameError
      # Create a new error
      #
      # @param [String] presenter_class_name Class name of presenter
      # @param [String] presenter_file_path File path where the presenter Ruby class was found at
      def initialize(presenter_class_name, presenter_file_path)
        super("Presenter class #{presenter_class_name} is not defined but file exists at #{presenter_file_path}")
      end
    end

    class InvalidControllerError < NameError
      # Create a new error
      def initialize(controller)
        super("You tried to present a #{controller.class.name} class, but only a Rails controller or mailer can be presented")
      end
    end

    # @return [Class] Class constant for the built MetaPresenter::Base presenter
    def presenter_class
      # Try to find the presenter class (Not guaranteed,
      # for example if the presenter class wasn't defined
      # or if the file wasn't found)
      klass = nil
      ancestors.each do |klass_name|
        klass = presenter_class_for(klass_name)
        break unless klass.nil?
      end

      klass
    end

    def presenter_base_dir
      File.join(Rails.root, 'app', 'presenters')
    end

    private

    def all_ancestors
      controller.class.ancestors.select do |ancestor|
        if controller.class <= ActionController::Base
          ancestor <= ActionController::Base
        elsif controller.class <= ActionMailer::Base
          ancestor <= ActionMailer::Base
        else
          raise InvalidControllerError, controller
        end
      end
    end

    def mailer_ancestors
      ancestors_until(ApplicationMailer) do |klass|
        "Mailers::#{klass.name.gsub('Mailer', '')}"
      end
    end

    def controller_ancestors
      ancestors_until(ApplicationController) do |klass|
        klass.try(:name).try(:gsub, 'Controller', '')
      end
    end

    def presenter_class_for(klass_name)
      presenter_class_name = "::#{klass_name}Presenter"
      begin
        presenter_class_name.constantize

      # No corresponding presenter class was found
      rescue NameError => e
        filename = "#{presenter_class_name.underscore}.rb"
        presenter_file_path = File.join(presenter_base_dir, filename)
        if File.exist?(presenter_file_path)
          raise FileExistsButPresenterNotDefinedError.new(presenter_class_name, presenter_file_path)
        end

        nil
      end
    end

    def ancestors
      # Different ancestors depending on whether
      # we're dealing with a mailer or a controller
      list = if all_ancestors.map(&:to_s).include?('ActionMailer::Base')
               mailer_ancestors
             else
               controller_ancestors
             end

      # add a presenter class for our current action
      # to the front of the list
      presenter_class_name_for_current_action = "#{list.first}::#{action_name.camelcase}"
      list.unshift(presenter_class_name_for_current_action)
    end

    # The list of ancestors is very long.
    # Trim it down to just the length of the class we are looking for.
    #
    # Takes an optional block method to transform the result
    # with a map operation
    def ancestors_until(until_class, &block)
      # trim down the list
      ancestors_list = all_ancestors[0..all_ancestors.index(until_class)]

      # map to the fully qualified class name
      ancestors_list.map(&block)
    end
  end
end