ruby-grape/grape

View on GitHub
lib/grape/router.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

require 'grape/router/route'
require 'grape/router/greedy_route'
require 'grape/util/cache'

module Grape
  class Router
    attr_reader :map, :compiled

    def self.normalize_path(path)
      path = +"/#{path}"
      path.squeeze!('/')
      path.sub!(%r{/+\Z}, '')
      path = '/' if path == ''
      path
    end

    def self.supported_methods
      @supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
    end

    def initialize
      @neutral_map = []
      @neutral_regexes = []
      @map = Hash.new { |hash, key| hash[key] = [] }
      @optimized_map = Hash.new { |hash, key| hash[key] = // }
    end

    def compile!
      return if compiled

      @union = Regexp.union(@neutral_regexes)
      @neutral_regexes = nil
      self.class.supported_methods.each do |method|
        routes = map[method]
        @optimized_map[method] = routes.map.with_index do |route, index|
          route.index = index
          Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
        end
        @optimized_map[method] = Regexp.union(@optimized_map[method])
      end
      @compiled = true
    end

    def append(route)
      map[route.request_method] << route
    end

    def associate_routes(pattern, **options)
      @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
      @neutral_map << Grape::Router::GreedyRoute.new(pattern: pattern, index: @neutral_map.length, **options)
    end

    def call(env)
      with_optimization do
        response, route = identity(env)
        response || rotation(env, route)
      end
    end

    def recognize_path(input)
      any = with_optimization { greedy_match?(input) }
      return if any == default_response

      any.endpoint
    end

    private

    def identity(env)
      route = nil
      response = transaction(env) do |input, method|
        route = match?(input, method)
        process_route(route, env) if route
      end
      [response, route]
    end

    def rotation(env, exact_route = nil)
      response = nil
      input, method = *extract_input_and_method(env)
      map[method].each do |route|
        next if exact_route == route
        next unless route.match?(input)

        response = process_route(route, env)
        break unless cascade?(response)
      end
      response
    end

    def transaction(env)
      input, method = *extract_input_and_method(env)
      response = yield(input, method)

      return response if response && !(cascade = cascade?(response))

      last_neighbor_route = greedy_match?(input)

      # If last_neighbor_route exists and request method is OPTIONS,
      # return response by using #call_with_allow_headers.
      return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade

      route = match?(input, '*')

      return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route

      if route
        response = process_route(route, env)
        return response if response && !(cascade = cascade?(response))
      end

      return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route

      nil
    end

    def process_route(route, env)
      prepare_env_from_route(env, route)
      route.exec(env)
    end

    def make_routing_args(default_args, route, input)
      args = default_args || { route_info: route }
      args.merge(route.params(input))
    end

    def extract_input_and_method(env)
      input = string_for(env[Grape::Http::Headers::PATH_INFO])
      method = env[Grape::Http::Headers::REQUEST_METHOD]
      [input, method]
    end

    def with_optimization
      compile! unless compiled
      yield || default_response
    end

    def default_response
      [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
    end

    def match?(input, method)
      @optimized_map[method].match(input) { |m| @map[method].detect { |route| m["_#{route.index}"] } }
    end

    def greedy_match?(input)
      @union.match(input) { |m| @neutral_map.detect { |route| m["_#{route.index}"] } }
    end

    def call_with_allow_headers(env, route)
      prepare_env_from_route(env, route)
      env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
      route.endpoint.call(env)
    end

    def prepare_env_from_route(env, route)
      input, = *extract_input_and_method(env)
      env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
    end

    def cascade?(response)
      response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
    end

    def string_for(input)
      self.class.normalize_path(input)
    end
  end
end