lib/apipie/param_description.rb
module Apipie
# method parameter description
#
# name - method name (show)
# desc - description
# required - boolean if required
# validator - Validator::BaseValidator subclass
class ParamDescription
attr_reader :method_description, :name, :desc, :allow_nil, :allow_blank, :validator, :options, :metadata, :show, :as, :validations, :response_only, :request_only
attr_reader :additional_properties, :is_array
attr_accessor :parent, :required
alias response_only? response_only
alias request_only? request_only
alias is_array? is_array
def self.from_dsl_data(method_description, args)
param_name, validator, desc_or_options, options, block = args
Apipie::ParamDescription.new(method_description,
param_name,
validator,
desc_or_options,
options,
&block)
end
def to_s
"ParamDescription: #{method_description.id}##{name}"
end
def ==(other)
return false unless self.class == other.class
if method_description == other.method_description && @options == other.options
true
else
false
end
end
def initialize(method_description, name, validator, desc_or_options = nil, options = {}, &block)
if desc_or_options.is_a?(Hash)
options = options.merge(desc_or_options)
elsif desc_or_options.is_a?(String)
options[:desc] = desc_or_options
elsif !desc_or_options.nil?
raise ArgumentError.new("param description: expected description or options as 3rd parameter")
end
options.symbolize_keys!
# we save options to know what was passed in DSL
@options = options
if @options[:param_group]
@from_concern = @options[:param_group][:from_concern]
end
if validator.is_a?(Hash)
@options.merge!(validator.select{|k,v| k != :array_of })
end
@method_description = method_description
@name = concern_subst(name)
@as = options[:as] || @name
@desc = preformat_text(@options[:desc])
@parent = @options[:parent]
@metadata = @options[:meta]
@required = is_required?
@response_only = (@options[:only_in] == :response)
@request_only = (@options[:only_in] == :request)
raise ArgumentError.new("'#{@options[:only_in]}' is not a valid value for :only_in") if (!@response_only && !@request_only) && @options[:only_in].present?
@show = if @options.key? :show
@options[:show]
else
true
end
@allow_nil = @options[:allow_nil] || false
@allow_blank = @options[:allow_blank] || false
action_awareness
if validator
if (validator != Hash) && (validator.is_a? Hash) && (validator[:array_of])
@is_array = true
validator = validator[:array_of]
raise "an ':array_of =>' validator is allowed exclusively on response-only fields" unless @response_only
end
@validator = Validator::BaseValidator.find(self, validator, @options, block)
raise "Validator for #{validator} not found." unless @validator
end
@validations = Array(options[:validations]).map {|v| concern_subst(Apipie.markup_to_html(v)) }
@additional_properties = @options[:additional_properties]
@deprecated = @options[:deprecated] || false
end
def from_concern?
method_description.from_concern? || @from_concern
end
def normalized_value(value)
if value.is_a?(ActionController::Parameters) && !value.is_a?(Hash)
value.to_unsafe_hash
elsif value.is_a? Array
value.map { |v| normalized_value (v) }
else
value
end
end
def validate(value)
return true if allow_nil && value.nil?
return true if allow_blank && value.blank?
value = normalized_value(value)
if (!allow_nil && value.nil?) || (blank_forbidden? && value.blank?) || !validator.valid?(value)
error = validator.error
error = ParamError.new(error) unless error.is_a? StandardError
raise error
end
end
def blank_forbidden?
!Apipie.configuration.ignore_allow_blank_false && !allow_blank && !validator.ignore_allow_blank?
end
def process_value(value)
value = normalized_value(value)
if @validator.respond_to?(:process_value)
@validator.process_value(value)
else
value
end
end
def full_name
name_parts = parents_and_self.map{|p| p.name if p.show}.compact
return name.to_s if name_parts.blank?
return ([name_parts.first] + name_parts[1..-1].map { |n| "[#{n}]" }).join("")
end
# returns an array of all the parents: starting with the root parent
# ending with itself
def parents_and_self
ret = []
if self.parent
ret.concat(self.parent.parents_and_self)
end
ret << self
ret
end
def to_json(lang = nil)
hash = {
name: name.to_s,
full_name: full_name,
description: preformat_text(Apipie.app.translate(@options[:desc], lang)),
required: required,
allow_nil: allow_nil,
allow_blank: allow_blank,
validator: validator.to_s,
expected_type: validator.expected_type,
metadata: metadata,
show: show,
validations: validations,
deprecated: deprecated?
}
if deprecation.present?
hash[:deprecation] = deprecation.to_json
end
if sub_params = validator.params_ordered
hash[:params] = sub_params.map { |p| p.to_json(lang)}
end
hash
end
def merge_with(other_param_desc)
if self.validator && other_param_desc.validator
self.validator.merge_with(other_param_desc.validator)
else
self.validator ||= other_param_desc.validator
end
self
end
# merge param descriptions. Allows defining hash params on more places
# (e.g. in param_groups). For example:
#
# def_param_group :user do
# param :user, Hash do
# param :name, String
# end
# end
#
# param_group :user
# param :user, Hash do
# param :password, String
# end
def self.unify(params)
ordering = params.map(&:name)
params.group_by(&:name).map do |name, param_descs|
param_descs.reduce(&:merge_with)
end.sort_by { |param| ordering.index(param.name) }
end
def self.merge(target_params, source_params)
params_to_merge, params_to_add = source_params.partition do |source_param|
target_params.any? { |target_param| source_param.name == target_param.name }
end
unify(target_params + params_to_merge)
target_params.concat(params_to_add)
end
# action awareness is being inherited from ancestors (in terms of
# nested params)
def action_aware?
if @options.key?(:action_aware)
return @options[:action_aware]
elsif @parent
@parent.action_aware?
else
false
end
end
def as_action
if @options[:param_group] && @options[:param_group][:options] &&
@options[:param_group][:options][:as]
@options[:param_group][:options][:as].to_s
elsif @parent
@parent.as_action
else
@method_description.method
end
end
# makes modification that are based on the action that the param
# is defined for. Typical for required and allow_nil variations in
# create/update actions.
def action_awareness
if action_aware?
if !@options.key?(:allow_nil)
if @required
@allow_nil = false
else
@allow_nil = true
end
end
if as_action != "create"
@required = false
end
end
end
def concern_subst(string)
return string if string.nil? or !from_concern?
original = string
string = ":#{original}" if original.is_a? Symbol
replaced = method_description.resource.controller._apipie_perform_concern_subst(string)
return original if replaced == string
return replaced.to_sym if original.is_a? Symbol
return replaced
end
def preformat_text(text)
concern_subst(Apipie.markup_to_html(text || ''))
end
def is_required?
if @options.key?(:required)
if (@options[:required] == true) || (@options[:required] == false)
@options[:required]
else
Array(@options[:required]).include?(@method_description.method.to_sym)
end
else
Apipie.configuration.required_by_default?
end
end
def deprecated?
@deprecated.present?
end
def deprecation
return if @deprecated.blank? || @deprecated == true
case @deprecated
when Hash
Apipie::ParamDescription::Deprecation.new(
info: @deprecated[:info],
deprecated_in: @deprecated[:in],
sunset_at: @deprecated[:sunset]
)
when String
Apipie::ParamDescription::Deprecation.new(info: @deprecated)
end
end
end
end