lib/oas_rails/extractors/route_extractor.rb
module OasRails module Extractors class RouteExtractor RAILS_DEFAULT_CONTROLLERS = %w[ rails/info rails/mailers active_storage/blobs active_storage/disk active_storage/direct_uploads active_storage/representations rails/conductor/continuous_integration rails/conductor/multiple_databases rails/conductor/action_mailbox rails/conductor/action_text action_cable ].freeze RAILS_DEFAULT_PATHS = %w[ /rails/action_mailbox/ ].freeze class << self def host_routes_by_path(path) @host_routes ||= extract_host_routes @host_routes.select { |r| r.path == path } end def host_routes @host_routes ||= extract_host_routes end # Clear Class Instance Variable @host_routes # # This method clear the class instance variable @host_routes # to force a extraction of the routes again. def clear_cache @host_routes = nil end def host_paths @host_paths ||= host_routes.map(&:path).uniq.sort end def clean_route(route) route.gsub('(.:format)', '').gsub(/:\w+/) { |match| "{#{match[1..]}}" } end # THIS CODE IS NOT IN USE BUT CAN BE USEFULL WITH GLOBAL TAGS OR AUTH TAGS # def get_controller_comments(controller_path) # YARD.parse_string(File.read(controller_path)) # controller_class = YARD::Registry.all(:class).first # if controller_class # class_comment = controller_class.docstring.all # method_comments = controller_class.meths.map do |method| # { # name: method.name, # comment: method.docstring.all # } # end # YARD::Registry.clear # { # class_comment: class_comment, # method_comments: method_comments # } # else # YARD::Registry.clear # nil # end # rescue StandardError # nil # end # # def get_controller_comment(controller_path) # get_controller_comments(controller_path)&.dig(:class_comment) || '' # rescue StandardError # '' # end private def extract_host_routes valid_routes.map { |r| OasRoute.new_from_rails_route(rails_route: r) } end def valid_routes Rails.application.routes.routes.select do |route| valid_api_route?(route) end end def valid_api_route?(route) return false unless valid_route_implementation?(route) return false if RAILS_DEFAULT_CONTROLLERS.any? { |default| route.defaults[:controller].start_with?(default) } return false if RAILS_DEFAULT_PATHS.any? { |path| route.path.spec.to_s.include?(path) } return false unless route.path.spec.to_s.start_with?(OasRails.config.api_path) return false if ignore_custom_actions(route) true end # Checks if a route has a valid implementation. # # This method verifies that both the controller and the action specified # in the route exist. It checks if the controller class is defined and # if the action method is implemented within that controller. # # @param route [ActionDispatch::Journey::Route] The route to check. # @return [Boolean] true if both the controller and action exist, false otherwise. def valid_route_implementation?(route) controller_name = route.defaults[:controller]&.camelize action_name = route.defaults[:action] return false if controller_name.blank? || action_name.blank? controller_class = "#{controller_name}Controller".safe_constantize if controller_class.nil? false else controller_class.instance_methods.include?(action_name.to_sym) end end # Ignore user-specified paths in initializer configuration. # Sanitize api_path by removing the "/" if it starts with that, and adding "/" if it ends without that. # Support controller name only to ignore all controller actions. # Support ignoring "controller#action" # Ignoring "controller#action" AND "api_path/controller#action" def ignore_custom_actions(route) api_path = "#{OasRails.config.api_path.sub(%r{\A/}, '')}/".sub(%r{/+$}, '/') ignored_actions = OasRails.config.ignored_actions.flat_map do |custom_route| if custom_route.start_with?(api_path) [custom_route] else ["#{api_path}#{custom_route}", custom_route] end end controller_action = "#{route.defaults[:controller]}##{route.defaults[:action]}" controller_only = route.defaults[:controller] ignored_actions.include?(controller_action) || ignored_actions.include?(controller_only) end end end end end