We are loading code dynamically with concerns, based on some environment variables, which works pretty nice.
Something like this:
# User class
class User
include DynamicConcern
end
module DynamicConcern
extend ActiveSupport::Concern
included do
if "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{self.name}Concern".safe_constantize
include "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{self.name}Concern".constantize
end
end
end
# custom code
module Custom::Custom123::UserConcern
extend ActiveSupport::Concern
included do
...
end
end
We are using this since years and it worked absolutely fine in models. Some days ago we tried to use the same approach with Controllers, but realized that this approach doesn' t work fine with inheritance, where the parent class inherits the concern as well as the inherited class:
class ApplicationController < ActionController::Base
# this gets loaded and includes the right dynamic module
include DynamicConcern
end
class ShopController < ApplicationController
# this is NOT getting loaded again and skipped,
# since it has been loaded already in the parent controller
include DynamicConcern
end
Is there a way to tell rails that it should include/evaluade the concern a second time, since the second time it would have another class name which would include another module?
I'm not looking for other solutions, since a lot of our code is based on this approach and I think it's possible to solve this without rewriting everything.
Thanks!
CodePudding user response:
You are only trying to dynamically include modules based on the class name.
It's not necessary to make a concern but it can be a normal class, and the include action can be a normal method. Every time you want to call it, just call it like any other method.
Because you have already written your code with ActiveSupport::Concern
in an include
fashion. I guess the following refactor may work even though I cannot guarantee it. The idea is simple:
- Just make it a normal method with the target class as the parameter. You can include it (it automatically calls
dynamic_include
inincluded
hook). - If the module is already included in the ancestor hierarchy chain, just invoke the
dynamic_include
will immediately call the method and do the dynamic includes.
Please give it a try and let me know if it works for your scenarios.
module DynamicConcern
extend ActiveSupport::Concern
included do
def self.dynamic_include(klass)
if "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{klass.name}Concern".safe_constantize
klass.include "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{klass.name}Concern".constantize
end
end
dynamic_include(self)
end
end
class ApplicationController < ActionController::Base
# this gets loaded and includes the right dynamic module
include DynamicConcern
end
class ShopController < ApplicationController
# this is NOT getting loaded again and skipped,
# since it has been loaded already in the parent controller
dynamic_include(self)
end
CodePudding user response:
Actually it's a feature of Rails that the same module doesn't get loaded multiple times.
We started to use the normal ruby module inclution hooks and it worked fine!
module CustomConcern
def self.included(base)
custom_class_lookup_paths = [
"#{HOSTNAME.camelize}::Models::#{base.name}PrependConcern",
"#{HOSTNAME.camelize}::Controllers::#{base.name}PrependConcern"
].map{|class_string| class_string.safe_constantize }.compact
custom_class_lookup_paths.each do |class_string|
base.send :include, class_string
end
end