lib/contrast/framework/rails/support.rb in contrast-agent-4.3.2 vs lib/contrast/framework/rails/support.rb in contrast-agent-4.4.0
- old
+ new
@@ -45,36 +45,38 @@
def collect_routes
find_all_routes(::Rails.application, [])
end
# Find the current route, based on the provided Request wrapper
+ #
# @param request[Contrast::Agent::Request]
# @return [Contrast::Api::Dtm::RouteCoverage]
def current_route request
return unless ::Rails.cs__respond_to?(:application)
- # returns array of arrays [[match_data, path_parameters, route]], sorted by
- # precedence
- # match_data: ActionDispatch::Journey::Path::Pattern::MatchData
- # path_parameters: hash of various things
- # route: ActionDispatch::Journey::Route
- full_routes = ::Rails.application.routes.router.send(:find_routes, request.rack_request)
- return if full_routes.empty?
+ match, _params, route, path = get_full_route(request.rack_request)
- full_route = full_routes[0]
+ original_url = request.rack_request.path_info
- # the route is directly implemented within the application
- if direct_route?(full_route)
- route = full_route[2] # route w/ highest precedence
- Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route)
- else
- engine_route(full_route, request)
+ # Route is either the final rails route, or a router that points to a Sinatra controller.
+ if Contrast::Framework::Sinatra::Support.sinatra_controller?(route.app.app)
+ # Create a request copied from current request, but with the base path removed from path_info.
+ new_req = ::ActionDispatch::Request.new(request.env)
+ new_req.path_info = new_req.path_info.gsub((path << match).join, '')
+
+ return Contrast::Framework::Sinatra::Support.current_route(new_req, route.app.app, original_url)
end
+
+ Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route, original_url)
rescue StandardError => _e
nil
end
+ # Copy a request for modification.
+ #
+ # @param [::ActionDispatch::Request] original env.
+ # @return [::ActionDispatch::Request] a copy of original env with rails env merged.
def retrieve_request env
rails_env = ::Rails.application.env_config.merge(env)
::ActionDispatch::Request.new(rails_env || env)
end
@@ -85,40 +87,37 @@
env[AC_INSTANCE].cs__class.included_modules.include?(::ActionController::Live)
end
private
- # route is not mounted within an engine
- def direct_route? full_route
- full_route[2]&.app&.cs__class == ActionDispatch::Routing::RouteSet::Dispatcher ||
- (full_route[2].cs__class == ActionDispatch::Journey::Route && full_route[2]&.app&.cs__class == ActionDispatch::Routing::Mapper::Constraints)
+ # Determine if route is a Rails engine route.
+ #
+ # @param [Object] app or route that points to a ::Rails::Engine
+ # @return [bool] whether the router is an engine or not.
+ def engine_route? route
+ route.app.is_a?(::ActionDispatch::Routing::Mapper::Constraints) && route.app.app < ::Rails::Engine
end
- def engine_route full_route, request
- engine_route = full_route[2] # supposed route - but actually an Engine mount point
- return unless engine_route
+ # Recursively get final route traversing engines as required.
+ #
+ # @param request [::Rack::Request] the rack request as will be handed to rails controller.
+ # @param top_router [::ActionDispatch::Journer::Router] the current router relative to the previous.
+ # @param path [Array<String>] the chunks of path that have been seen.
+ # @return [Array<array>] the final set of rails route classes.
+ def get_full_route request, top_router = ::Rails.application.routes.router, path = []
+ return if (route_matches = top_router.send(:find_routes, request)).empty?
- engine_mount_name = engine_route.name
- return unless engine_mount_name
+ match, params, route = route_matches.first
- engine_path_segments = request.rack_request.path_info.split(engine_mount_name)
- return if engine_path_segments.empty?
-
- path_within_engine = engine_path_segments[-1]
- return unless path_within_engine
-
- engine_router = engine_route.app&.app&.routes&.router
- return unless engine_router
-
- # Get all routes regardless of http method
- matching_routes = engine_router.send(:filter_routes, path_within_engine)
- return unless matching_routes
-
- # filter for current http method
- reportable_routes = engine_router.send(:match_routes, matching_routes, request.rack_request)
- return if reportable_routes.empty?
-
- Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(reportable_routes[0])
+ # If the current routing node points to a sub-app (::Rais::Engine), dive deeper.
+ # Have sub-app route the remainder of the url.
+ if engine_route?(route)
+ new_req = retrieve_request request.env
+ new_req.path_info = new_req.path_info.gsub(match.to_s, '')
+ get_full_route(new_req, route.app.app.routes.router, path << match.to_s)
+ else
+ [match, params, route, path]
+ end
end
# Rails engine routes need to be detected by inspecting Engine class route set
def find_all_routes app, route_list
return route_list unless app.cs__respond_to?(:routes) && app.routes.cs__respond_to?(:routes)