lucie-lib/lib/lucie/app.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Lucie

  #
  # Lucie::App represents the application context. When a lucie application starts,
  # it requires information about its environment, like root path, env, command. App
  # also responsible for loading the right controller and to execute the process how the
  # input will be parsed and the business logic is executed.
  #
  class App

    class << self
      #
      # Let the exception leave the application when any occur.
      #
      # Normally the application doesn't raise any exception, just stops quietly.
      # When the application is under debugging, developer needs more, detailed description
      # about the problem. Suggested to turn this attribute on during developing.
      #
      #  App.raise_exception = true
      #
      attr_accessor :raise_exception

      # Sets the log level of the application.
      #
      attr_accessor :log_level

      #
      # Root directory of the source of application.
      #
      # Template and other source paths are calculated relatively to this directory.
      #
      attr_accessor :root

      @raise_exception = false
      @log_level = :info
    end

    # Command line input that is being executed. Usually it equals with ARGV.
    #
    attr_reader :command

    # Root of the application. Same as App.root, it's a reference for that.
    #
    attr_reader :root

    # The directory where from the application has been executed.
    #
    attr_reader :pwd

    # Initializes and starts the applicaiton. Shortcut for self.init && self.start
    #
    def self.run(command = ARGV, root = nil, pwd = nil)
      root ||= File.expand_path("..", File.dirname(Kernel.caller[0]))
      instance = self.init(command, root, pwd)
      self.start(instance)
    end

    # Initializes the application.
    #
    # @param command [Array or String]
    # @param root [String] Path of the root of the app. [default: current application's path]
    # @param pwd [String] Path of the terminal position.
    #
    def self.init(command = ARGV, root = nil, pwd = nil)
      root ||= File.expand_path("..", File.dirname(Kernel.caller[0]))
      self.new(command, root, pwd)
    end

    # Starts the application instance and returns its exit status.
    #
    # @param instance [App] Application instance
    #
    def self.start(instance)
      instance.start
      instance.exit_value
    end

    def initialize(command, root, pwd = nil)
      @root = root
      @command = Core::CommandLineParser.new(command)
      @exit_value ||= 0
      @task = nil
      @pwd = pwd || ENV["PWD"]
      Dir.chdir(@pwd)
    end

    # Starts the application.
    def start
      help? ? call_help : call_method_invoking_process
    end

    # Exit status of the application
    def exit_value
      @exit_value
    end

    # Environment of the application. Contains the ENV of the terminal.
    def env
      ENV
    end

private

    def call_method_invoking_process
      autoload_controllers
      apply_validators
      pair_parameters
      call_action_on_controller
    rescue => e
      self.exception = e
      give_user_friendly_error_message
      write_backtrace
      raise if self.class.raise_exception
    end

    def help?
      @command.has_option?("-h") || @command.has_option?("--help") || @command.has_arg?("help") || task == "help"
    end

    def task
      @task ||= @command.shift
    end

    def action
      @action ||= begin
        @command.args.first ? @command.args.first.to_sym : :index
      end
    end

    def controller
      @controller ||= controller_class.send(:new, command, self)
    end

    def controller_class
      controller_class = begin
        if task
          [task.split("_").map{|i| i.capitalize}.join, "Controller"].join
        else
          @action = :help
          "ApplicationController"
        end
      end

      begin
        Object.const_get(controller_class.to_sym)
      rescue NameError
        self.exit_value = 255
        raise ControllerNotFound.new task
      end
    end

    def autoload_controllers
      base_path = File.join([root, "app/controllers"])
      $LOAD_PATH << base_path
      Dir.glob(File.join([base_path, "*_controller.rb"])).each do |path|
        filename = File.basename(path)
        controller_name = filename.split(".rb").first
        if controller_name.to_s.length > 0
          const_name = Helpers::ControllerName.new(controller_name).capitalized

          # place the constant to the root namespace
          Object.class_eval do
            autoload const_name.to_sym, controller_name
          end
        end
      end
    end

    def call_action_on_controller
      begin
        self.exit_value = controller.send(:apply_action, action, command)
      rescue ActionNotFound
        self.exit_value = controller.send(:apply_action, :no_method)
      end
    rescue ActionNotFound
      raise ActionNotFound.new(action, task)
    end

    def apply_validators
      controller.class.apply_validators
    end

    def pair_parameters
      controller.class.pair_parameters
    end

    def exception=(exception)
      @exception = exception
    end

    def give_user_friendly_error_message
      $stderr << @exception.to_s.strip
    end

    def write_backtrace
      $stderr.puts Kernel.caller.join("\n") if App.log_level == :debug
    end

    def call_help
      $stdout << controller.help
    end

    def exit_value=(exit_value)
      @exit_value = exit_value if exit_value.is_a?(Fixnum)
    end
  end
end