Apipie/apipie-rails

View on GitHub
lib/apipie/validator.rb

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: utf-8 -*-

module Apipie

  module Validator

    # to create new validator, inherit from Apipie::Validator::BaseValidator
    # and implement class method build and instance method validate
    class BaseValidator

      attr_accessor :param_description

      def initialize(param_description)
        @param_description = param_description
      end

      def inspected_fields
        [:param_description]
      end

      def inspect
        string = "#<#{self.class.name}:#{self.object_id} "
        fields = inspected_fields.map {|field| "#{field}: #{self.send(field)}"}
        string << fields.join(", ") << ">"
      end

      def self.inherited(subclass)
        @validators ||= []
        @validators.insert 0, subclass
      end

      # find the right validator for given options
      def self.find(param_description, argument, options, block)
        @validators.each do |validator_type|
          validator = validator_type.build(param_description, argument, options, block)
          return validator if validator
        end
        return nil
      end

      def self.raise_if_missing_params
        missing_params = []
        yield missing_params
        if missing_params.size > 1
          raise ParamMultipleMissing.new(missing_params)
        elsif missing_params.size == 1
          raise ParamMissing.new(missing_params.first)
        end
      end

      # check if value is valid
      def valid?(value)
        if self.validate(value)
          @error_value = nil
          true
        else
          @error_value = value
          false
        end
      end

      def param_name
        @param_description.name
      end

      # validator description
      def description
        "TODO: validator description"
      end

      def format_description_value(value)
        "<code>#{CGI::escapeHTML(value.to_s)}</code>"
      end

      def error
        ParamInvalid.new(param_name, @error_value, description)
      end

      def to_s
        self.description
      end

      def to_json
        self.description
      end

      # what type is expected, mostly string
      # this information is used in cli client
      # thor supported types :string, :hash, :array, :numeric, or :boolean
      def expected_type
        'string'
      end

      def ignore_allow_blank?
        false
      end

      def merge_with(other_validator)
        return self if self == other_validator
        raise NotImplementedError, "Don't know how to merge #{self.inspect} with #{other_validator.inspect}"
      end

      def params_ordered
        nil
      end

      def ==(other)
        return false unless self.class == other.class
        if param_description == other.param_description
          true
        else
          false
        end
      end
    end

    # validate arguments type
    class TypeValidator < BaseValidator

      def initialize(param_description, argument)
        super(param_description)
        @type = argument
      end

      def validate(value)
        return false if value.nil?
        value.is_a? @type
      end

      def self.build(param_description, argument, options, block)
        if argument.is_a?(Class) && (argument != Hash || block.nil?)
          self.new(param_description, argument)
        end
      end

      def description
        "Must be a #{@type}"
      end

      def expected_type
        if @type.ancestors.include? Hash
          'hash'
        elsif @type.ancestors.include? Array
          'array'
        elsif @type.ancestors.include? Numeric
          'numeric'
        elsif @type.ancestors.include? File
          'file'
        else
          'string'
        end
      end
    end

    # validate arguments value with regular expression
    class RegexpValidator < BaseValidator

      def initialize(param_description, argument)
        super(param_description)
        @regexp = argument
      end

      def validate(value)
        value =~ @regexp
      end

      def self.build(param_description, argument, options, proc)
        self.new(param_description, argument) if argument.is_a? Regexp
      end

      def description
        "Must match regular expression #{format_description_value("/#{@regexp.source}/")}."
      end
    end

    # arguments value must be one of given in array
    class EnumValidator < BaseValidator
      def initialize(param_description, argument)
        super(param_description)
        @array = argument
      end

      def validate(value)
        @array.include?(value)
      end

      def self.build(param_description, argument, options, proc)
        self.new(param_description, argument) if argument.is_a?(Array)
      end

      def values
        @array
      end

      def description
        string = @array.map { |value| format_description_value(value) }.join(', ')
        "Must be one of: #{string}."
      end
    end

    # arguments value must be an array
    class ArrayValidator < Apipie::Validator::BaseValidator
      def initialize(param_description, argument, options = {})
        super(param_description)
        @type = argument
        @items_type = options[:of]
        @items_enum = options[:in]
      end

      def validate(values)
        return false unless process_value(values).respond_to?(:each) && !process_value(values).is_a?(String)
        process_value(values).all? { |v| validate_item(v)}
      end

      def process_value(values)
        values || []
      end

      def description
        "Must be an array of #{items}"
      end

      def expected_type
        "array"
      end

      def self.build(param_description, argument, options, block)
        if argument == Array && !block.is_a?(Proc)
          self.new(param_description, argument, options)
        end
      end

      private

      def enum
        if @items_enum.kind_of?(Proc)
          @items_enum = Array(@items_enum.call)
        end
        @items_enum
      end

      def validate_item(value)
        has_valid_type?(value) &&
          is_valid_value?(value)
      end

      def has_valid_type?(value)
        if @items_type
          item_validator = BaseValidator.find('', @items_type, nil, nil)

          if item_validator
            item_validator.valid?(value)
          else
            value.kind_of?(@items_type)
          end
        else
          true
        end
      end

      def is_valid_value?(value)
        if enum
          enum.include?(value)
        else
          true
        end
      end

      def items
        unless enum
          @items_type || "any type"
        else
          enum.inspect
        end
      end
    end

    class ArrayClassValidator < BaseValidator

      def initialize(param_description, argument)
        super(param_description)
        @array = argument
      end

      def validate(value)
        @array.include?(value.class)
      end

      def self.build(param_description, argument, options, block)
        if argument.is_a?(Array) && argument.first.class == Class && !block.is_a?(Proc)
          self.new(param_description, argument)
        end
      end

      def description
        string = @array.map { |value| format_description_value(value) }.join(', ')
        "Must be one of: #{string}."
      end
    end

    class ProcValidator < BaseValidator

      def initialize(param_description, argument)
        super(param_description)
        @proc = argument
      end

      def validate(value)
        (@help = @proc.call(value)) === true
      end

      def self.build(param_description, argument, options, proc)
        self.new(param_description, argument) if argument.is_a?(Proc) && argument.arity == 1
      end

      def error
        ParamInvalid.new(param_name, @error_value, @help)
      end

      def description
        ""
      end
    end

    class HashValidator < BaseValidator
      include Apipie::DSL::Base
      include Apipie::DSL::Param

      def self.build(param_description, argument, options, block)
        self.new(param_description, block, options[:param_group]) if block.is_a?(Proc) && block.arity <= 0 && argument == Hash
      end

      def initialize(param_description, argument, param_group)
        super(param_description)
        @proc = argument
        @param_group = param_group
        self.instance_exec(&@proc)
        # specifying action_aware on Hash influences the child params,
        # not the hash param itself: assuming it's required when
        # updating as well
        if param_description.options[:action_aware] && param_description.options[:required]
          param_description.required = true
        end
        prepare_hash_params
      end

      def params_ordered
        @params_ordered ||= _apipie_dsl_data[:params].map do |args|
          options = args.find { |arg| arg.is_a? Hash }
          options[:parent] = self.param_description
          options[:param_group] = @param_group
          Apipie::ParamDescription.from_dsl_data(param_description.method_description, args)
        end
      end

      def validate(value)
        return false if !value.is_a? Hash

        BaseValidator.raise_if_missing_params do |missing|
          @hash_params&.each do |k, p|
            if Apipie.configuration.validate_presence?
              missing << p if p.required && !value.key?(k)
            end
            if Apipie.configuration.validate_value?
              p.validate(value[k]) if value.key?(k)
            end
          end
        end

        return true
      end

      def process_value(value)
        if @hash_params && value
          return @hash_params.each_with_object({}) do |(key, param), api_params|
            if value.key?(key)
              api_params[param.as] = param.process_value(value[key])
            end
          end
        end
      end

      def description
        "Must be a Hash"
      end

      def expected_type
        'hash'
      end

      # where the group definition should be looked up when no scope
      # given. This is expected to return a controller.
      def _default_param_group_scope
        @param_group && @param_group[:scope]
      end

      def merge_with(other_validator)
        if other_validator.is_a? HashValidator
          @params_ordered = ParamDescription.unify(self.params_ordered + other_validator.params_ordered)
          prepare_hash_params
        else
          super
        end
      end

      def prepare_hash_params
        @hash_params = params_ordered.reduce({}) do |h, param|
          h.update(param.name.to_sym => param)
        end
      end
    end


    # special type of validator: we say that it's not specified
    class UndefValidator < BaseValidator

      def validate(value)
        true
      end

      def self.build(param_description, argument, options, block)
        if argument == :undef
          self.new(param_description)
        end
      end

      def description
        nil
      end
    end

    class DecimalValidator < BaseValidator

      def validate(value)
        self.class.validate(value)
      end

      def self.build(param_description, argument, options, block)
        if argument == :decimal
          self.new(param_description)
        end
      end

      def description
        "Must be a decimal number."
      end

      def expected_type
        'numeric'
      end

      def self.validate(value)
        value.to_s =~ /\A^[-+]?[0-9]+([,.][0-9]+)?\Z$/
      end
    end

    class NumberValidator < BaseValidator

      def validate(value)
        self.class.validate(value)
      end

      def self.build(param_description, argument, options, block)
        if argument == :number
          self.new(param_description)
        end
      end

      def description
        "Must be a number."
      end

      def expected_type
        'numeric'
      end

      def self.validate(value)
        value.to_s =~ /\A(0|[1-9]\d*)\Z$/
      end
    end

    class BooleanValidator < BaseValidator

      def validate(value)
        %w[true false 1 0].include?(value.to_s)
      end

      def self.build(param_description, argument, options, block)
        if argument == :bool || argument == :boolean || boolean_array?(argument)
          self.new(param_description)
        end
      end

      private_class_method def self.boolean_array?(argument)
        argument.is_a?(Array) && (argument - [true, false]) == []
      end

      def description
        string = %w(true false 1 0).map { |value| format_description_value(value) }.join(', ')
        "Must be one of: #{string}."
      end

      def ignore_allow_blank?
        true
      end

      def expected_type
        'boolean'
      end
    end

    class NestedValidator < BaseValidator

      def initialize(param_description, argument, param_group)
        super(param_description)
        @validator = Apipie::Validator::HashValidator.new(param_description, argument, param_group)
        @type = argument
      end

      def validate(value)
        value ||= [] # Rails convert empty array to nil
        return false if value.class != Array
        value.each do |child|
          return false unless @validator.validate(child)
        end
        true
      end

      def process_value(value)
        value ||= [] # Rails convert empty array to nil
        @values = []
        value.each do |child|
          @values << @validator.process_value(child)
        end
        @values
      end

      def self.build(param_description, argument, options, block)
        # in Ruby 1.8.x the arity on block without args is -1
        # while in Ruby 1.9+ it is 0
        self.new(param_description, block, options[:param_group]) if block.is_a?(Proc) && block.arity <= 0 && argument == Array
      end

      def expected_type
        'array'
      end

      def description
        "Must be an Array of nested elements"
      end

      def params_ordered
        @validator.params_ordered
      end
    end

  end
end