Rails Reserved Dynamic Controllers in Rails

I have a somewhat fancy requirement for a new Rails application. I need to create an application in which all routes are defined in several namespaces (let me explain). I want to have an application in which school subjects (math, English, etc.) are namespaces:

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

This is great and it works, but for me I need to create a StudentsController namespace for each object, which means that if I add a new object, then I need to create a new controller.

I would like to create a Base::StudentsController , and if, say, Math::StudentsController exists, then it will be used, and if it does not exist, we can dynamically create this controller and inherit from Base::StudentsController .

Is it possible? If so, how can I implement this?

+6
source share
6 answers

I ended up writing some custom logic in ActionDispatch::Routing::RouteSet::Dispatcher.controller_reference . I try to find all the constants needed for this controller and create them if they are missing. This FAR code is 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 
+1
source

With routes defined as follows:

 %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.

+3
source

I believe this will do:

  %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 Math::StudentsController , /english/students/ goes to English::StudentsController , and all other objects (e.g. /physics/students and /cs/students ) go to Base::StudentsController .

I think it’s exactly what you want and it only adds one line of code to the original solution.

+3
source

To confirm your requirements:

  • Minimum ads for each topic / resource
  • Use a dedicated controller ( Math::StudentsController ), if one exists, otherwise use a base controller ( StudentsController )

Rails expects each route to have a dedicated controller, and there really is no good way to support the second requirement. So, so 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
source

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

to create a base :: StudentsController

and if, say, there is Math :: StudentsController

then it will be used

and if it does not exist, then we can dynamically create this controller and inherit from Base :: StudentsController

You can achieve this by adding dynamic constant search ( const_missing ) and defining a dynamic constant with inheritance ( Object.const_set ).

I imagine something like that; with several settings and more stringent verification, it will 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 

This will add dynamic constant requests to ActionDispatch::Routing::RouteSet , from which Dynamicroutes::Application.routes is inherited, therefore undefined constants in Dynamicroutes::Application.routes.draw will generate the corresponding classes, subclasses from Base::StudentsController .

+3
source

All routing helpers, such as resources , scope , etc., are just functions inside your application routes. You can simply define a custom function as follows:

 YourApplication.routes.draw do # Let 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 work equally well in all 3.x versions.

I have included no null checks or begin/rescue blocks anywhere. Since you are going to use this custom function only when necessary, I assume that you will pass the correct and necessary parameters. If you said that you passed a fallback controller that does not exist, I would prefer that the route parsing fail, rather than trying to process it.

Edit: Typo in function arguments

Edit 2: Forget &block in function arguments

Edit 3: Add "_controller" to target_controller variable

+1
source

Source: https://habr.com/ru/post/944650/


All Articles