# frozen_string_literal: true
module ApipieDSL
module Validator
class Lazy
def initialize(param_description, argument, options, block)
@param_description = param_description
@argument = argument
@options = options
@block = block
end
def build
# TODO support for plain Ruby
return unless defined? Rails
BaseValidator.find(@param_description, @argument.constantize, @options, @block)
end
end
# To create a new validator, inherit from ApipieDSL::Validator::BaseValidator
# and implement class method 'build' and instance method 'validate'
class BaseValidator
attr_reader :param_description
def initialize(param_description)
@param_description = param_description
end
def self.build(_param_description, _argument, _options, &_block)
raise NotImplementedError
end
def validate(_value)
raise NotImplementedError
end
def description
raise NotImplementedError
end
def inspected_fields
[:param_description]
end
def inspect
string = "#<#{self.class.name}:#{object_id} "
fields = inspected_fields.map { |field| "#{field}: #{send(field)}" }
string << fields.join(', ') << '>'
end
def self.inherited(subclass)
@validators ||= []
@validators.unshift(subclass)
end
def self.find(param_description, argument, options, block)
@validators.each do |type|
validator = type.build(param_description, argument, options, block)
return validator if validator
end
nil
end
def valid?(value)
return true if validate(value)
raise ParamInvalid.new(@param_description.name, value, description)
end
def to_s
description
end
def docs
raise NotImplementedError
end
def expected_type
'string'
end
def sub_params
nil
end
def merge_with(other_validator)
return self if self == other_validator
raise NotImplementedError, "Don't know how to merge #{inspect} with #{other_validator.inspect}"
end
def ==(other)
return false unless self.class == other.class
param_description == other.param_description
end
end
class TypeValidator < BaseValidator
def initialize(param_description, argument)
super(param_description)
@type = argument
end
def self.build(param_description, argument, _options, block)
return unless argument.is_a?(::Class)
return if argument == Hash && !block.nil?
new(param_description, argument)
end
def validate(value)
return false if value.nil?
value.is_a?(@type)
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'
else
'string'
end
end
end
class RegexpValidator < BaseValidator
def initialize(param_description, argument)
super(param_description)
@regexp = argument
end
def self.build(param_description, argument, _options, _block)
new(param_description, argument) if argument.is_a?(Regexp)
end
def validate(value)
value =~ @regexp
end
def description
"Must match regular expression /#{@regexp.source}/
."
end
def expected_type
'regexp'
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 self.build(param_description, argument, _options, _block)
new(param_description, argument) if argument.is_a?(Array)
end
def validate(value)
@array.include?(value)
end
def values
@array
end
def description
string = @array.map { |value| "#{value}
" }.join(', ')
"Must be one of: #{string}."
end
end
class ArrayValidator < BaseValidator
def initialize(param_description, argument, options = {})
super(param_description)
@type = argument
@items_type = options[:of]
@items_enum = options[:in]
end
def self.build(param_description, argument, options, block)
return if argument != Array || block.is_a?(Proc)
new(param_description, argument, options)
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_type}"
end
def expected_type
'array'
end
private
def validate_item(value)
valid_type?(value) && valid_value?(value)
end
def valid_type?(value)
return true unless @items_type
item_validator = BaseValidator.find(nil, @items_type, nil, nil)
if item_validator
item_validator.valid?(value)
else
value.is_a?(@items_type)
end
end
def items_enum
@items_enum = Array(@items_enum.call) if @items_enum.is_a?(Proc)
@items_enum
end
def valid_value?(value)
if items_enum
items_enum.include?(value)
else
true
end
end
def items_type
return items_enum.inspect if items_enum
@items_type || 'any type'
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)
return if !argument.is_a?(Array) || argument.first.class != ::Class || block.is_a?(Proc)
new(param_description, argument)
end
def description
"Must be one of: #{@array.join(', ')}."
end
end
class ProcValidator < BaseValidator
def initialize(param_description, argument)
super(param_description)
@proc = argument
end
def validate(value)
# The proc should return true if value is valid
# Otherwise it should return a string
!(@help = @proc.call(value)).is_a?(String)
end
def self.build(param_description, argument, _options, _block)
return if !argument.is_a?(Proc) || argument.arity != 1
new(param_description, argument)
end
def description
@help
end
end
class HashValidator < BaseValidator
include ApipieDSL::Base
include ApipieDSL::Parameter
include ApipieDSL::Klass
def initialize(param_description, argument, param_group)
super(param_description)
@param_group = param_group
instance_exec(&argument)
prepare_hash_params
end
def self.build(param_description, argument, options, block)
return if argument != Hash || !block.is_a?(Proc) || block.arity.positive?
new(param_description, block, options[:param_group])
end
def sub_params
@sub_params ||= dsl_data[:params].map do |args|
options = args.find { |arg| arg.is_a?(Hash) }
options[:parent] = param_description
ApipieDSL::ParameterDescription.from_dsl_data(param_description.method_description, args)
end
end
def validate(value)
return false unless value.is_a?(Hash)
@hash_params&.each do |name, param|
if ApipieDSL.configuration.validate_value?
param.validate(value[name]) if value.key?(name)
end
end
true
end
def description
'Must be a Hash'
end
def expected_type
'hash'
end
def default_param_group_scope
@param_group && @param_group[:scope]
end
def merge_with(other_validator)
if other_validator.is_a?(HashValidator)
@sub_params = ApipieDSL::ParameterDescription.unify(sub_params + other_validator.sub_params)
prepare_hash_params
else
super
end
end
private
def prepare_hash_params
@hash_params = sub_params.each_with_object({}) do |param, hash|
hash.update(param.name.to_sym => param)
end
end
end
class DecimalValidator < BaseValidator
def self.build(param_description, argument, _options, _block)
return if argument != :decimal
new(param_description)
end
def validate(value)
value.to_s =~ /\A^[-+]?[0-9]+([,.][0-9]+)?\Z$/
end
def description
'Must be a decimal number'
end
end
class NumberValidator < BaseValidator
def self.build(param_description, argument, _options, _block)
return if argument != :number
new(param_description)
end
def validate(value)
value.to_s =~ /\A(0|[1-9]\d*)\Z$/
end
def description
'Must be a number'
end
def expected_type
'numeric'
end
end
class BooleanValidator < BaseValidator
def self.build(param_description, argument, _options, _block)
return unless %i[bool boolean].include?(argument)
new(param_description)
end
def validate(value)
%w[true false 1 0].include?(value.to_s)
end
def description
string = %w[true false 1 0].map { |value| "#{value}
" }.join(', ')
"Must be one of: #{string}"
end
def expected_type
'boolean'
end
end
class RestValidator < BaseValidator
def self.build(param_description, argument, _options, _block)
return unless %i[rest list splat].include?(argument)
new(param_description)
end
def validate(_value)
# In *rest param we don't care about passed values.
true
end
def description
'Must be a list of values'
end
def expected_type
'list'
end
end
class NestedValidator < BaseValidator
def initialize(param_description, argument, param_group)
super(param_description)
@validator = HashValidator.new(param_description, argument, param_group)
@type = argument
end
def self.build(param_description, argument, options, block)
return if argument != Array || !block.is_a?(Proc) || block.arity.positive?
new(param_description, block, options[:param_group])
end
def validate(value)
value ||= []
return false if value.class != Array
value.each do |child|
return false unless @validator.validate(child)
end
true
end
def expected_type
'array'
end
def description
'Must be an Array of nested elements'
end
def sub_params
@validator.sub_params
end
end
end
end