lib/avo/resources/resource_manager.rb
module Avo module Resources class ResourceManager attr_accessor :resources alias_method :all, :resources class << self def build instance = new instance.init_resources instance.check_bad_resources instance end # Fetches the resources available to the application. # We have two ways of doing that. # # 1. Through eager loading. # We automatically eager load the resources directory and fetch the descendants from the scanned files. # This is the simple way to get started. # # 2. Manually, declared by the user. # We have this option to load the resources because when they are loaded automatically through eager loading, # those Resource classes and their methods may trigger loading other classes. And that may disrupt Rails booting process. # Ex: AdminResource may use self.model_class = User. That will trigger Ruby to load the User class and itself load # other classes in a chain reaction. # The scenario that comes up most often is when Rails boots, the routes are being computed which eager loads the resource files. # At that boot time some migration might have not been run yet, but Rails tries to access them through model associations, # and they are not available. # # To enable this feature add a `resources` array config in your Avo initializer. # config.resources = [ # "UserResource", # "FishResource", # ] def fetch_resources resources = if Avo.configuration.resources.nil? BaseResource.descendants else Avo.configuration.resources end resources.map do |resource| if resource.is_a?(Class) resource else resource.to_s.safe_constantize end end end end def initialize @resources = [] end def init_resources self.resources = self.class.fetch_resources .reject do |resource| # Remove the BaseResource. We only need the descendants resource == Avo::BaseResource end .uniq do |klass| # On invalid resource configuration the resource classes get duplicated in `ObjectSpace` # We need to de-duplicate them klass.name end .map do |resource| resource.new if resource.is_a? Class end end def check_bad_resources resources.each do |resource| has_model = resource.model_class.present? unless has_model possible_model = resource.class.to_s.gsub "Avo::Resources::", "" possible_model = possible_model.gsub "Resource", "" Avo::App.errors.add({ url: "https://docs.avohq.io/2.0/resources.html#custom-model-class", target: "_blank", message: "#{resource.class} does not have a valid model assigned. It failed to find the #{possible_model} model. \n\r Please create that model or assign one using self.model_class = YOUR_MODEL" }) end end end # Filters out the resources that are missing the model_class def valid_resources resources .select do |resource| resource.model_class.present? end .sort_by(&:to_s) end # Returns the Avo resource by camelized name # # get_resource_by_name('User') => Avo::Resources::User def get_resource(resource) resource = "Avo::Resources::#{resource}" unless resource.to_s.starts_with?("Avo::Resources::") resources.find do |available_resource| resource.to_s == available_resource.class.to_s end end # Returns the Avo resource by singular snake_cased name # # get_resource_by_name('user') => Avo::Resources::User def get_resource_by_name(name) get_resource name.singularize.camelize end # Returns the Avo resource by singular snake_cased name # From all the resources that use the same model_class, it will fetch the first one in alphabetical order # # get_resource_by_name('User') => Avo::Resources::User # get_resource_by_name(User) => Avo::Resources::User def get_resource_by_model_class(klass) # Fetch the mappings imposed by the user. # If they are present, use those ones. mapping = get_mapping_for_model klass return get_resource(mapping) if mapping.present? valid_resources .find do |resource| resource.model_class.model_name.name == klass.to_s end end # Returns the Avo resource by singular snake_cased name # # get_resource_by_controller_name('delayed_backend_active_record_jobs') => DelayedJobResource # get_resource_by_controller_name('users') => Avo::Resources::User def get_resource_by_controller_name(name) valid_resources .find do |resource| resource.model_class.to_s.pluralize.underscore.tr("/", "_") == name.to_s end end # Returns the Avo resource by some name def guess_resource(name) get_resource_by_name(name.to_s) || get_resource_by_model_class(name) end # Returns the Rails model class by singular snake_cased name # # get_model_class_by_name('user') => User def get_model_class_by_name(name) name.to_s.camelize.singularize end def get_available_resources(user = nil) valid_resources .select do |resource| Services::AuthorizationService.authorize user, resource.model_class, Avo.configuration.authorization_methods.stringify_keys["index"], raise_exception: false end .sort_by { |r| r.name } end def resources_for_navigation(user = nil) get_available_resources(user) .select do |resource| resource.visible_on_sidebar end end private def get_mapping_for_model(klass) (Avo.configuration.model_resource_mapping || {}).stringify_keys.transform_values(&:to_s)[klass.to_s] end end end end