ruby-grape/grape

View on GitHub
lib/grape/validations/types.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

require 'grape/validations/types/json'
require 'grape/validations/types/file'

module Grape
  module Validations
    # Module for code related to grape's system for
    # coercion and type validation of incoming request
    # parameters.
    #
    # Grape uses a number of tests and assertions to
    # work out exactly how a parameter should be handled,
    # based on the +type+ and +coerce_with+ options that
    # may be supplied to {Grape::Dsl::Parameters#requires}
    # and {Grape::Dsl::Parameters#optional}. The main
    # entry point for this process is {Types.build_coercer}.
    module Types
      module_function

      PRIMITIVES = [
        # Numerical
        Integer,
        Float,
        BigDecimal,
        Numeric,

        # Date/time
        Date,
        DateTime,
        Time,

        # Misc
        Grape::API::Boolean,
        String,
        Symbol,
        TrueClass,
        FalseClass
      ].freeze

      # Types representing data structures.
      STRUCTURES = [Hash, Array, Set].freeze

      SPECIAL = {
        ::JSON => Json,
        Array[JSON] => JsonArray,
        ::File => File,
        Rack::Multipart::UploadedFile => File
      }.freeze

      GROUPS = [Array, Hash, JSON, Array[JSON]].freeze

      # Is the given class a primitive type as recognized by Grape?
      #
      # @param type [Class] type to check
      # @return [Boolean] whether or not the type is known by Grape as a valid
      #   type for a single value
      def primitive?(type)
        PRIMITIVES.include?(type)
      end

      # Is the given class a standard data structure (collection or map)
      # as recognized by Grape?
      #
      # @param type [Class] type to check
      # @return [Boolean] whether or not the type is known by Grape as a valid
      #   data structure type
      def structure?(type)
        STRUCTURES.include?(type)
      end

      # Is the declared type in fact an array of multiple allowed types?
      # For example the declaration +types: [Integer,String]+ will attempt
      # first to coerce given values to integer, but will also accept any
      # other string.
      #
      # @param type [Array<Class>,Set<Class>] type (or type list!) to check
      # @return [Boolean] +true+ if the given value will be treated as
      #   a list of types.
      def multiple?(type)
        (type.is_a?(Array) || type.is_a?(Set)) && type.size > 1
      end

      # Does Grape provide special coercion and validation
      # routines for the given class? This does not include
      # automatic handling for primitives, structures and
      # otherwise recognized types. See {Types::SPECIAL}.
      #
      # @param type [Class] type to check
      # @return [Boolean] +true+ if special routines are available
      def special?(type)
        SPECIAL.key? type
      end

      # Is the declared type a supported group type?
      # Currently supported group types are Array, Hash, JSON, and Array[JSON]
      #
      # @param type [Array<Class>,Class] type to check
      # @return [Boolean] +true+ if the type is a supported group type
      def group?(type)
        GROUPS.include? type
      end

      # A valid custom type must implement a class-level `parse` method, taking
      # one String argument and returning the parsed value in its correct type.
      #
      # @param type [Class] type to check
      # @return [Boolean] whether or not the type can be used as a custom type
      def custom?(type)
        !primitive?(type) &&
          !structure?(type) &&
          !multiple?(type) &&
          type.respond_to?(:parse) &&
          type.method(:parse).arity == 1
      end

      # Is the declared type an +Array+ or +Set+ of a {#custom?} type?
      #
      # @param type [Array<Class>,Class] type to check
      # @return [Boolean] true if +type+ is a collection of a type that implements
      #   its own +#parse+ method.
      def collection_of_custom?(type)
        (type.is_a?(Array) || type.is_a?(Set)) &&
          type.length == 1 &&
          (custom?(type.first) || special?(type.first))
      end

      def map_special(type)
        SPECIAL.fetch(type, type)
      end

      # Chooses the best coercer for the given type. For example, if the type
      # is Integer, it will return a coercer which will be able to coerce a value
      # to the integer.
      #
      # There are a few very special coercers which might be returned.
      #
      # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when
      # the given type implies values in an array with different types.
      # For example, +[Integer, String]+ allows integer and string values in
      # an array.
      #
      # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when
      # a method is specified by a user with +coerce_with+ option or the user
      # specifies a custom type which implements requirments of
      # +Grape::Types::CustomTypeCoercer+.
      #
      # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the
      # previous one, but it expects an array or set of values having a custom
      # type implemented by the user.
      #
      # There is also a group of custom types implemented by Grape, check
      # +Grape::Validations::Types::SPECIAL+ to get the full list.
      #
      # @param type [Class] the type to which input strings
      #   should be coerced
      # @param method [Class,#call] the coercion method to use
      # @return [Object] object to be used
      #   for coercion and type validation
      def build_coercer(type, method: nil, strict: false)
        cache_instance(type, method, strict) do
          create_coercer_instance(type, method, strict)
        end
      end

      def create_coercer_instance(type, method, strict)
        # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!!
        type = Types.map_special(type)

        # Use a special coercer for multiply-typed parameters.
        if Types.multiple?(type)
          MultipleTypeCoercer.new(type, method)

          # Use a special coercer for custom types and coercion methods.
        elsif method || Types.custom?(type)
          CustomTypeCoercer.new(type, method)

          # Special coercer for collections of types that implement a parse method.
          # CustomTypeCoercer (above) already handles such types when an explicit coercion
          # method is supplied.
        elsif Types.collection_of_custom?(type)
          Types::CustomTypeCollectionCoercer.new(
            Types.map_special(type.first), type.is_a?(Set)
          )
        else
          DryTypeCoercer.coercer_instance_for(type, strict)
        end
      end

      def cache_instance(type, method, strict, &_block)
        key = cache_key(type, method, strict)

        return @__cache[key] if @__cache.key?(key)

        instance = yield

        @__cache_write_lock.synchronize do
          @__cache[key] = instance
        end

        instance
      end

      def cache_key(type, method, strict)
        [type, method, strict].each_with_object(+'_') do |val, memo|
          next if val.nil?

          memo << '_' << val.to_s
        end
      end

      instance_variable_set(:@__cache,            {})
      instance_variable_set(:@__cache_write_lock, Mutex.new)
    end
  end
end