Dynamic namespaced controllers w/ fallback in Rails

拈花ヽ惹草 提交于 2019-12-23 09:31:14

问题


I have a somewhat bizarre requirement for a new Rails application. I need to build an application in which all routes are defined in multiple namespaces (let me explain). I want to have an application in which school subjects (math, english, etc) are the namespaces:

%w[math english].each do |subject|
  namespace subject.to_sym do
    resources :students
  end
end

This is great and it works but it requires me to create a namespaced StudentsController for each subject which means if I add a new subject then I need to create a new controller.

What I would like is to create a Base::StudentsController and if, let's say the Math::StudentsController exists then it will be used and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController.

Is this something that is possible? If so then how would I go about implementing this?


回答1:


With routes defined this way:

%w[math english].each do |subject|
  scope "/#{subject}" do
    begin
      "#{subject.camelcase}::StudentsController".constantize
      resources :students, controller: "#{subject}::students", only: :index
    rescue
      resources :students, controller: "base::students", only: :index
    end
  end
end

rake routes outputs:

students GET /math/students(.:format)    base::students#index
         GET /english/students(.:format) english::students#index

if english/students_controller.rb is present and math/students_controller. is absent.




回答2:


To restate your requirements:

  1. Minimal declarations per subject/resource pair
  2. Use dedicated controller (Math::StudentsController) if it exists, otherwise use base controller (StudentsController)

Rails expects each route to have a dedicated controller, and doesn't really have a good way to support the second requirement. So, this is how I would do it:

Dynamicroutes::Application.routes.draw do
  SUBJECTS = [ "math", "english", "chemistry" ]
  RESOURCES = [ "assignments", "students" ]

  class DedicatedSubjectResourceControllerConstraint
    def initialize(subject, resource)
      @subject = subject
      @resource = resource
    end

    def matches?(request)
      begin
        defined?("#{@subject.capitalize}::#{@resource.capitalize}")
        return true
      rescue NameError
        Rails.logger.debug "No such class: #{@subject.capitalize}::#{@resource.capitalize}"
        return false
      end
    end
  end

  class ValidSubjectConstraint
    def matches?(request)
      return SUBJECTS.include?(request.path_parameters[:subject])
    end
  end

  SUBJECTS.each do |subject|
    RESOURCES.each do |resource|      
      namespace subject, :constraints => DedicatedSubjectResourceControllerConstraint.new(subject, resource) do
        resources resource
      end
    end
  end

  RESOURCES.each do |resource|
    scope "/:subject", :constraints => ValidSubjectConstraint.new do
      resources resource
    end
  end
end



回答3:


This sounds like a use for const_missing. If what you want to do is

to create a Base::StudentsController

and if, let's say the Math::StudentsController exists

then it will be used

and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController

You can achieve that by adding dynamic constant lookup (const_missing) and dynamic constant definition with inheritance (Object.const_set).

I imagine something like this; with a few tweaks and more rigorous checking, would work:

# initializers/dynamic_controllers.rb

class ActionDispatch::Routing::RouteSet

  SUBJECTS = [ "math", "english", "chemistry" ]

  def const_missing(name, *args, &block)
    if SUBJECTS.any?{ |subject| name.include? subject.uppercase }
      Object.const_set name, Class.new(Base::StudentsController)
    else
      super
    end
  end

end

That'll add dynamic constant lookups to ActionDispatch::Routing::RouteSet, from which Dynamicroutes::Application.routes inherits, so undefined constants in Dynamicroutes::Application.routes.draw will generate the corresponding classes subclassed from Base::StudentsController.




回答4:


I believe this will do it:

  %w[math english].each do |subject|
    namespace subject.to_sym do
      resources :students
    end
  end

  match ':subject/students(/:action(/:id))' => 'base/students'

With these combined routes, /math/students goes to the Math::StudentsController, /english/students/ goes to the English::StudentsController, and all other subjects (e.g. /physics/students and /cs/students) go to the Base::StudentsController.

Which I think is exactly what you want and only adds one line of code to your original solution.




回答5:


All the routing helpers like resources, scope, etc are just functions inside your application's routes. You could just define a custom function as follows:

YourApplication.routes.draw do

  # Let's define a custom method that you're going to use for your specific needs
  def resources_with_fallback(*args, &block)
    target_module       = @scope[:module].camelize.constantize
    target_controller   = "#{args.first.to_s}_controller".camelize
    fallback_controller = args.last.delete(:fallback).to_s.camelize.constantize

    # Create the target controller class
    # using fallback_controller as the superclass
    # if it doesn't exist
    unless target_module.const_defined?(target_controller)
      target_module.const_set target_controller, Class.new(fallback_controller)
    end

    # Call original resources method
    resources *args, &block
  end

  # Now go ahead and define your routes!

  namespace "test" do
    namespace "new" do
      # Use our custom_resources function and pass a fallback parameter
      custom_resources :photos, :fallback => 'base/some_controller'
    end
  end

end

I tested this in Rails 3.2, but it should equally work well in all 3.x versions.

I included no null checks or begin/rescue blocks anywhere. Since you're going to use this custom function only when required, I'm assuming that you will pass the correct and necessary parameters. If say you passed a fallback controller that doesn't exist, I'd rather that the routes parsing fail with an exception, rather than trying to handle it.

Edit: Typo in function arguments

Edit 2: Forgot &block in function arguments

Edit 3: Add "_controller" to the target_controller variable




回答6:


I ended up writing some custom logic into ActionDispatch::Routing::RouteSet::Dispatcher.controller_reference. I attempt to look up all of the constants required for the given controller and create them if they're missing. This code is FAR from perfect so please feel free to edit w/ improvements.

class ActionDispatch::Routing::RouteSet::Dispatcher

  private

  def controller_reference(controller_param)
    const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"

    obj = Object
    const_name.split('::').each do |cn|
      begin
        obj =  obj.const_get(cn)
      rescue
        if obj == Object
          obj =  obj.const_set(cn, Class.new(ApplicationController))
        else
          puts "Creating #{obj}::#{cn} based on Generic::#{cn}"
          obj =  obj.const_set(cn, Class.new("Generic::#{cn}".constantize))
        end
      end
    end

    ActiveSupport::Dependencies.constantize(const_name)
  end

end


来源:https://stackoverflow.com/questions/16468030/dynamic-namespaced-controllers-w-fallback-in-rails

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!