# frozen_string_literal: true require "active_support/dependencies" require "active_support/core_ext/name_error" module AbstractController module Helpers extend ActiveSupport::Concern included do class_attribute :_helper_methods, default: Array.new # This is here so that it is always higher in the inheritance chain than # the definition in lib/action_view/rendering.rb redefine_singleton_method(:_helpers) do if @_helpers ||= nil @_helpers else superclass._helpers end end self._helpers = define_helpers_module(self) end class MissingHelperError < LoadError def initialize(error, path) @error = error @path = "helpers/#{path}.rb" set_backtrace error.backtrace if /^#{path}(\.rb)?$/.match?(error.path) super("Missing helper file helpers/%s.rb" % path) else raise error end end end def _helpers self.class._helpers end module ClassMethods # When a class is inherited, wrap its helper module in a new module. # This ensures that the parent class's module can be changed # independently of the child class's. def inherited(klass) # Inherited from parent by default klass._helpers = nil klass.class_eval { default_helper_module! } unless klass.anonymous? super end attr_writer :_helpers # Declare a controller method as a helper. For example, the following # makes the +current_user+ and +logged_in?+ controller methods available # to the view: # class ApplicationController < ActionController::Base # helper_method :current_user, :logged_in? # # private # def current_user # @current_user ||= User.find_by(id: session[:user]) # end # # def logged_in? # current_user != nil # end # end # # In a view: # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> # # ==== Parameters # * method[, method] - A name or names of a method on the controller # to be made available on the view. def helper_method(*methods) methods.flatten! self._helper_methods += methods location = caller_locations(1, 1).first file, line = location.path, location.lineno methods.each do |method| # def current_user(*args, &block) # controller.send(:'current_user', *args, &block) # end _helpers_for_modification.class_eval <<~ruby_eval.lines.map(&:strip).join(";"), file, line def #{method}(*args, &block) controller.send(:'#{method}', *args, &block) end ruby2_keywords(:'#{method}') ruby_eval end end # Includes the given modules in the template class. # # Modules can be specified in different ways. All of the following calls # include +FooHelper+: # # # Module, recommended. # helper FooHelper # # # String/symbol without the "helper" suffix, camel or snake case. # helper "Foo" # helper :Foo # helper "foo" # helper :foo # # The last two assume that "foo".camelize returns "Foo". # # When strings or symbols are passed, the method finds the actual module # object using String#constantize. Therefore, if the module has not been # yet loaded, it has to be autoloadable, which is normally the case. # # Namespaces are supported. The following calls include +Foo::BarHelper+: # # # Module, recommended. # helper Foo::BarHelper # # # String/symbol without the "helper" suffix, camel or snake case. # helper "Foo::Bar" # helper :"Foo::Bar" # helper "foo/bar" # helper :"foo/bar" # # The last two assume that "foo/bar".camelize returns "Foo::Bar". # # The method accepts a block too. If present, the block is evaluated in # the context of the controller helper module. This simple call makes the # +wadus+ method available in templates of the enclosing controller: # # helper do # def wadus # "wadus" # end # end # # Furthermore, all the above styles can be mixed together: # # helper FooHelper, "woo", "bar/baz" do # def wadus # "wadus" # end # end # def helper(*args, &block) modules_for_helpers(args).each do |mod| next if _helpers.include?(mod) _helpers_for_modification.include(mod) end _helpers_for_modification.module_eval(&block) if block_given? end # Clears up all existing helpers in this class, only keeping the helper # with the same name as this class. def clear_helpers inherited_helper_methods = _helper_methods self._helpers = Module.new self._helper_methods = Array.new inherited_helper_methods.each { |meth| helper_method meth } default_helper_module! unless anonymous? end # Given an array of values like the ones accepted by +helper+, this method # returns an array with the corresponding modules, in the same order. def modules_for_helpers(modules_or_helper_prefixes) modules_or_helper_prefixes.flatten.map! do |module_or_helper_prefix| case module_or_helper_prefix when Module module_or_helper_prefix when String, Symbol helper_prefix = module_or_helper_prefix.to_s helper_prefix = helper_prefix.camelize unless helper_prefix.start_with?(/[A-Z]/) "#{helper_prefix}Helper".constantize else raise ArgumentError, "helper must be a String, Symbol, or Module" end end end def _helpers_for_modification unless @_helpers self._helpers = define_helpers_module(self, superclass._helpers) end _helpers end private def define_helpers_module(klass, helpers = nil) # In some tests inherited is called explicitly. In that case, just # return the module from the first time it was defined return klass.const_get(:HelperMethods) if klass.const_defined?(:HelperMethods, false) mod = Module.new klass.const_set(:HelperMethods, mod) mod.include(helpers) if helpers mod end def default_helper_module! helper_prefix = name.delete_suffix("Controller") helper(helper_prefix) rescue NameError => e raise unless e.missing_name?("#{helper_prefix}Helper") end end end end