intridea/grape

View on GitHub
lib/grape/api/instance.rb

Summary

Maintainability
B
5 hrs
Test Coverage
# frozen_string_literal: true

module Grape
  class API
    # The API Instance class, is the engine behind Grape::API. Each class that inherits
    # from this will represent a different API instance
    class Instance
      include Grape::DSL::API

      class << self
        attr_reader :instance, :base
        attr_accessor :configuration

        def given(conditional_option, &block)
          evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block
        end

        def mounted(&block)
          evaluate_as_instance_with_configuration(block, lazy: true)
        end

        def base=(grape_api)
          @base = grape_api
          grape_api.instances << self
        end

        def to_s
          base&.to_s || super
        end

        def base_instance?
          self == base.base_instance
        end

        # A class-level lock to ensure the API is not compiled by multiple
        # threads simultaneously within the same process.
        LOCK = Mutex.new

        # Clears all defined routes, endpoints, etc., on this API.
        def reset!
          reset_endpoints!
          reset_routes!
          reset_validations!
        end

        # Parses the API's definition and compiles it into an instance of
        # Grape::API.
        def compile
          @instance ||= new
        end

        # Wipe the compiled API so we can recompile after changes were made.
        def change!
          @instance = nil
        end

        # This is the interface point between Rack and Grape; it accepts a request
        # from Rack and ultimately returns an array of three values: the status,
        # the headers, and the body. See [the rack specification]
        # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
        def call(env)
          compile!
          call!(env)
        end

        # A non-synchronized version of ::call.
        def call!(env)
          instance.call(env)
        end

        # (see #cascade?)
        def cascade(value = nil)
          if value.nil?
            inheritable_setting.namespace_inheritable.key?(:cascade) ? !namespace_inheritable(:cascade).nil? : true
          else
            namespace_inheritable(:cascade, value)
          end
        end

        def compile!
          return if instance

          LOCK.synchronize { compile unless instance }
        end

        # see Grape::Router#recognize_path
        def recognize_path(path)
          compile!
          instance.router.recognize_path(path)
        end

        protected

        def prepare_routes
          endpoints.map(&:routes).flatten
        end

        # Execute first the provided block, then each of the
        # block passed in. Allows for simple 'before' setups
        # of settings stack pushes.
        def nest(*blocks, &block)
          blocks.compact!
          if blocks.any?
            evaluate_as_instance_with_configuration(block) if block
            blocks.each { |b| evaluate_as_instance_with_configuration(b) }
            reset_validations!
          else
            instance_eval(&block)
          end
        end

        def evaluate_as_instance_with_configuration(block, lazy: false)
          lazy_block = Grape::Util::Lazy::Block.new do |configuration|
            value_for_configuration = configuration
            self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy?
            response = instance_eval(&block)
            self.configuration = value_for_configuration
            response
          end
          if base && base_instance? && lazy
            lazy_block
          else
            lazy_block.evaluate_from(configuration)
          end
        end

        def inherited(subclass)
          subclass.reset!
          subclass.logger = logger.clone
        end

        def inherit_settings(other_settings)
          top_level_setting.inherit_from other_settings.point_in_time_copy

          # Propagate any inherited params down to our endpoints, and reset any
          # compiled routes.
          endpoints.each do |e|
            e.inherit_settings(top_level_setting.namespace_stackable)
            e.reset_routes!
          end

          reset_routes!
        end
      end

      attr_reader :router

      # Builds the routes from the defined endpoints, effectively compiling
      # this API into a usable form.
      def initialize
        @router = Router.new
        add_head_not_allowed_methods_and_options_methods
        self.class.endpoints.each do |endpoint|
          endpoint.mount_in(@router)
        end

        @router.compile!
        @router.freeze
      end

      # Handle a request. See Rack documentation for what `env` is.
      def call(env)
        result = @router.call(env)
        result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade?
        result
      end

      # Some requests may return a HTTP 404 error if grape cannot find a matching
      # route. In this case, Grape::Router adds a X-Cascade header to the response
      # and sets it to 'pass', indicating to grape's parents they should keep
      # looking for a matching route on other resources.
      #
      # In some applications (e.g. mounting grape on rails), one might need to trap
      # errors from reaching upstream. This is effectivelly done by unsetting
      # X-Cascade. Default :cascade is true.
      def cascade?
        return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.key?(:cascade)
        return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options)&.key?(:cascade)

        true
      end

      reset!

      private

      # For every resource add a 'OPTIONS' route that returns an HTTP 204 response
      # with a list of HTTP methods that can be called. Also add a route that
      # will return an HTTP 405 response for any HTTP method that the resource
      # cannot handle.
      def add_head_not_allowed_methods_and_options_methods
        versioned_route_configs = collect_route_config_per_pattern
        # The paths we collected are prepared (cf. Path#prepare), so they
        # contain already versioning information when using path versioning.
        # Disable versioning so adding a route won't prepend versioning
        # informations again.
        without_root_prefix do
          without_versioning do
            versioned_route_configs.each do |config|
              next if config[:options][:matching_wildchar]

              allowed_methods = config[:methods].dup

              allowed_methods |= [Grape::Http::Headers::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Grape::Http::Headers::GET)

              allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods)

              config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)

              attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
              generate_not_allowed_method(config[:pattern], **attributes)
            end
          end
        end
      end

      def collect_route_config_per_pattern
        all_routes       = self.class.endpoints.map(&:routes).flatten
        routes_by_regexp = all_routes.group_by { |route| route.pattern.to_regexp }

        # Build the configuration based on the first endpoint and the collection of methods supported.
        routes_by_regexp.values.map do |routes|
          last_route        = routes.last # Most of the configuration is taken from the last endpoint
          matching_wildchar = routes.any? { |route| route.request_method == '*' }
          {
            options: { matching_wildchar: matching_wildchar },
            pattern: last_route.pattern,
            requirements: last_route.requirements,
            path: last_route.origin,
            endpoint: last_route.app,
            methods: matching_wildchar ? Grape::Http::Headers::SUPPORTED_METHODS : routes.map(&:request_method)
          }
        end
      end

      # Generate a route that returns an HTTP 405 response for a user defined
      # path on methods not specified
      def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
        supported_methods =
          if self.class.namespace_inheritable(:do_not_route_options)
            Grape::Http::Headers::SUPPORTED_METHODS
          else
            Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS
          end
        not_allowed_methods = supported_methods - allowed_methods
        @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
      end

      # Allows definition of endpoints that ignore the versioning configuration
      # used by the rest of your API.
      def without_versioning(&_block)
        old_version = self.class.namespace_inheritable(:version)
        old_version_options = self.class.namespace_inheritable(:version_options)

        self.class.namespace_inheritable_to_nil(:version)
        self.class.namespace_inheritable_to_nil(:version_options)

        yield

        self.class.namespace_inheritable(:version, old_version)
        self.class.namespace_inheritable(:version_options, old_version_options)
      end

      # Allows definition of endpoints that ignore the root prefix used by the
      # rest of your API.
      def without_root_prefix(&_block)
        old_prefix = self.class.namespace_inheritable(:root_prefix)

        self.class.namespace_inheritable_to_nil(:root_prefix)

        yield

        self.class.namespace_inheritable(:root_prefix, old_prefix)
      end
    end
  end
end