# frozen_string_literal: true module InheritedResources module ClassMethods protected # Used to overwrite the default assumptions InheritedResources do. Whenever # this method is called, it should be on the top of your controller, since # almost other methods depends on the values given to <>defaults. # # == Options # # * :resource_class - The resource class which by default is guessed # by the controller name. Defaults to Project in # ProjectsController. # # * :collection_name - The name of the collection instance variable which # is set on the index action. Defaults to :projects in # ProjectsController. # # * :instance_name - The name of the singular instance variable which # is set on all actions besides index action. Defaults to # :project in ProjectsController. # # * :route_collection_name - The name of the collection route. Defaults to :collection_name. # # * :route_instance_name - The name of the singular route. Defaults to :instance_name. # # * :route_prefix - The route prefix which is automatically set in namespaced # controllers. Default to :admin on Admin::ProjectsController. # # * :singleton - Tells if this controller is singleton or not. # # * :finder - Specifies which method should be called to instantiate the resource. # # defaults :finder => :find_by_slug # def defaults(options) raise ArgumentError, 'Class method :defaults expects a hash of options.' unless options.is_a? Hash options.symbolize_keys! options.assert_valid_keys(:resource_class, :collection_name, :instance_name, :class_name, :route_prefix, :route_collection_name, :route_instance_name, :singleton, :finder) self.resource_class = options[:resource_class] if options.key?(:resource_class) self.resource_class = options[:class_name].constantize if options.key?(:class_name) acts_as_singleton! if options.delete(:singleton) config = self.resources_configuration[:self] if options.key?(:resource_class) or options.key?(:class_name) config[:request_name] = begin request_name = self.resource_class request_name = request_name.model_name.param_key if request_name.respond_to?(:model_name) request_name.to_s.underscore.tr('/', '_') end options.delete(:resource_class) and options.delete(:class_name) end options.each do |key, value| config[key] = value&.to_sym end create_resources_url_helpers! end # Defines which actions will be inherited from the inherited controller. # Syntax is borrowed from resource_controller. # # actions :index, :show, :edit # actions :all, :except => :index # def actions(*actions_to_keep) raise ArgumentError, 'Wrong number of arguments. You have to provide which actions you want to keep.' if actions_to_keep.empty? options = actions_to_keep.extract_options! actions_to_remove = Array(options[:except]) actions_to_remove += ACTIONS - actions_to_keep.map { |a| a.to_sym } unless actions_to_keep.first == :all actions_to_remove.map! { |a| a.to_sym }.uniq! (instance_methods.map { |m| m.to_sym } & actions_to_remove).each do |action| undef_method action, "#{action}!" end end # Defines that this controller belongs to another resource. # # belongs_to :projects # # == Options # # * :parent_class - Allows you to specify what is the parent class. # # belongs_to :project, :parent_class => AdminProject # # * :class_name - Also allows you to specify the parent class, but you should # give a string. Added for ActiveRecord belongs to compatibility. # # * :instance_name - The instance variable name. By default is the name of the association. # # belongs_to :project, :instance_name => :my_project # # * :finder - Specifies which method should be called to instantiate the parent. # # belongs_to :project, :finder => :find_by_title! # # This will make your projects be instantiated as: # # Project.find_by_title!(params[:project_id]) # # Instead of: # # Project.find(params[:project_id]) # # * :param - Allows you to specify params key to retrieve the id. # Default is :association_id, which in this case is :project_id. # # * :route_name - Allows you to specify what is the route name in your url # helper. By default is association name. # # * :collection_name - Tell how to retrieve the next collection. Let's # suppose you have Tasks which belongs to Projects # which belongs to companies. This will do somewhere # down the road: # # @company.projects # # But if you want to retrieve instead: # # @company.admin_projects # # You supply the collection name. # # * :polymorphic - Tell the association is polymorphic. # # * :singleton - Tell it's a singleton association. # # * :optional - Tell the association is optional (it's a special # type of polymorphic association) # def belongs_to(*symbols, &block) options = symbols.extract_options! options.symbolize_keys! options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param, :finder, :route_name, :collection_name, :singleton, :polymorphic, :optional, :shallow) optional = options.delete(:optional) shallow = options.delete(:shallow) polymorphic = options.delete(:polymorphic) finder = options.delete(:finder) if self.parents_symbols.empty? include BelongsToHelpers helper_method :parent, :parent? end acts_as_polymorphic! if polymorphic || optional acts_as_shallow! if shallow raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty? raise ArgumentError, "You cannot define multiple associations with options: #{options.keys.inspect} to belongs to." unless symbols.size == 1 || options.empty? symbols.each do |symbol| symbol = symbol.to_sym if polymorphic || optional self.parents_symbols << :polymorphic unless self.parents_symbols.include?(:polymorphic) self.resources_configuration[:polymorphic][:symbols] << symbol self.resources_configuration[:polymorphic][:optional] ||= optional else self.parents_symbols << symbol end self.resources_configuration[:self][:shallow] = true if shallow config = self.resources_configuration[symbol] = {} class_name = '' config[:parent_class] = options.delete(:parent_class) || begin class_name = if options[:class_name] options.delete(:class_name) else namespace = self.name.deconstantize model_name = symbol.to_s.pluralize.classify klass = model_name while namespace != '' new_klass = "#{namespace}::#{model_name}" if new_klass.safe_constantize klass = new_klass break else namespace = namespace.deconstantize end end klass = model_name if klass.start_with?('::') klass end class_name.constantize rescue NameError => e raise unless e.message.include?(class_name) nil end config[:singleton] = options.delete(:singleton) || false config[:collection_name] = options.delete(:collection_name) || symbol.to_s.pluralize.to_sym config[:instance_name] = options.delete(:instance_name) || symbol config[:param] = options.delete(:param) || :"#{symbol}_id" config[:route_name] = options.delete(:route_name) || symbol config[:finder] = finder || :find end if block class_eval(&block) else create_resources_url_helpers! end end alias :nested_belongs_to :belongs_to # A quick method to declare polymorphic belongs to. # def polymorphic_belongs_to(*symbols, &block) options = symbols.extract_options! options[:polymorphic] = true belongs_to(*symbols, options, &block) end # A quick method to declare singleton belongs to. # def singleton_belongs_to(*symbols, &block) options = symbols.extract_options! options[:singleton] = true belongs_to(*symbols, options, &block) end # A quick method to declare optional belongs to. # def optional_belongs_to(*symbols, &block) options = symbols.extract_options! options[:optional] = true belongs_to(*symbols, options, &block) end # Defines custom restful actions by resource or collection basis. # # custom_actions :resource => [:delete, :transit], :collection => :search # # == Options # # * :resource - Allows you to specify resource actions. # custom_actions :resource => :delete # This macro creates 'delete' method in controller and defines # delete_resource_{path,url} helpers. The body of generated 'delete' # method is same as 'show' method. So you can override it if need # # * :collection - Allows you to specify collection actions. # custom_actions :collection => :search # This macro creates 'search' method in controller and defines # search_resources_{path,url} helpers. The body of generated 'search' # method is same as 'index' method. So you can override it if need # def custom_actions(options) self.resources_configuration[:self][:custom_actions] = options options.each do | resource_or_collection, actions | [*actions].each do | action | create_custom_action(resource_or_collection, action) end end create_resources_url_helpers! [*options[:resource]].each do | action | helper_method "#{action}_resource_path", "#{action}_resource_url" end [*options[:collection]].each do | action | helper_method "#{action}_resources_path", "#{action}_resources_url" end end # Defines the role to use when creating or updating resource. # Makes sense when using rails 3.1 mass assignment conventions def with_role(role) self.resources_configuration[:self][:role] = role.try(:to_sym) end def without_protection(flag) self.resources_configuration[:self][:without_protection] = flag end private def acts_as_singleton! #:nodoc: unless self.resources_configuration[:self][:singleton] self.resources_configuration[:self][:singleton] = true include SingletonHelpers actions :all, except: :index end end def acts_as_polymorphic! #:nodoc: unless self.parents_symbols.include?(:polymorphic) include PolymorphicHelpers helper_method :parent_type, :parent_class end end def acts_as_shallow! #:nodoc: include BelongsToHelpers include ShallowHelpers end # Initialize resources class accessors and set their default values. # def initialize_resources_class_accessors! #:nodoc: # First priority is the namespaced model, e.g. User::Group self.resource_class ||= begin namespaced_class = self.name.sub(/Controller$/, '').singularize namespaced_class.constantize rescue NameError nil end # Second priority is the top namespace model, e.g. EngineName::Article for EngineName::Admin::ArticlesController self.resource_class ||= begin namespaced_classes = self.name.sub(/Controller$/, '').split('::') namespaced_class = [namespaced_classes.first, namespaced_classes.last].join('::').singularize namespaced_class.constantize rescue NameError nil end # Third priority the camelcased c, i.e. UserGroup self.resource_class ||= begin camelcased_class = self.name.sub(/Controller$/, '').gsub('::', '').singularize camelcased_class.constantize rescue NameError nil end # Otherwise use the Group class, or fail self.resource_class ||= begin class_name = self.controller_name.classify class_name.constantize rescue NameError => e raise unless e.message.include?(class_name) nil end self.parents_symbols = self.parents_symbols.try(:dup) || [] # Initialize resources configuration hash self.resources_configuration = self.resources_configuration.try(:dup) || {} self.resources_configuration.each do |key, value| next unless value.is_a?(Hash) || value.is_a?(Array) self.resources_configuration[key] = value.dup end config = (self.resources_configuration[:self] ||= {}) config[:collection_name] = self.controller_name.to_sym config[:instance_name] = self.controller_name.singularize.to_sym config[:route_collection_name] = config[:collection_name] config[:route_instance_name] = config[:instance_name] # Deal with namespaced controllers namespaces = self.controller_path.split('/')[0..-2] # Remove namespace if its a mountable engine namespaces.delete_if do |namespace| begin "#{namespace}/engine".camelize.constantize.isolated? rescue StandardError, LoadError false end end config[:route_prefix] = namespaces.join('_').to_sym unless namespaces.empty? # Deal with default request parameters in namespaced controllers, e.g. # Forum::Thread#create will properly pick up the request parameter # which will be forum_thread, and not thread # Additionally make this work orthogonally with instance_name config[:request_name] = self.resource_class.to_s.underscore.tr('/', '_') # Initialize polymorphic, singleton, scopes and belongs_to parameters polymorphic = self.resources_configuration[:polymorphic] || { symbols: [], optional: false } polymorphic[:symbols] = polymorphic[:symbols].dup self.resources_configuration[:polymorphic] = polymorphic end def create_custom_action(resource_or_collection, action) class_eval <<-CUSTOM_ACTION, __FILE__, __LINE__ def #{action}(options={}, &block) respond_with(*with_chain(#{resource_or_collection}), options, &block) end alias :#{action}! :#{action} protected :#{action}! CUSTOM_ACTION end # Hook called on inheritance. # def inherited(base) #:nodoc: super(base) base.send :initialize_resources_class_accessors! base.send :create_resources_url_helpers! end end end