lib/apipie/validator.rb
# -*- 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