# frozen_string_literal: true require 'rspec/rails/api/utils' require 'rspec/rails/api/open_api_renderer' require 'rspec/rails/api/entity_config' module RSpec module Rails module Api # Handles contexts and examples metadatas. class Metadata # rubocop:disable Metrics/ClassLength attr_reader :entities, :resources, :current_resource def initialize @resources = {} @entities = {} # Only used when building metadata during RSpec boot @current_resource = nil @current_method = nil @current_url = nil @current_code = nil end def add_resource(name, description) @resources[name.to_sym] = { description: description, paths: {} } @current_resource = name.to_sym end def add_entity(type, fields) Utils.deep_set(@resources, "#{@current_resource}.entities.#{type}", EntityConfig.new(fields)) end def add_path_params(fields) # rubocop:disable Metrics/MethodLength check_current_context :resource, :url chunks = @current_url.split('?') fields.each do |name, field| valid_attribute = Utils.check_attribute_type(field[:type], except: %i[array object]) raise "Field type not allowed: #{field[:type]}" unless valid_attribute scope = path_param_scope(chunks, name) Utils.deep_set(@resources, "#{@current_resource}.paths.#{@current_url}.path_params.#{name}", description: field[:description] || nil, type: field[:type] || nil, required: field[:required] || true, scope: scope) end end # Fields should be something like: # id: {type: :number, description: 'Something'}, # name: {type: string, description: 'Something'} # Ex. with sub elements: # id: {type: :number, description: 'Something'}, # something: {type: :object, description: 'Something', properties: { # property: {type: :string, description: 'Something'}, # ... # }} def add_request_params(fields) check_current_context :resource, :url, :method params = organize_params fields Utils.deep_set(@resources, "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.params", params) end def add_action(method, url, description) check_current_context :resource Utils.deep_set(@resources, "#{@current_resource}.paths.#{url}.actions.#{method}", description: description, statuses: {}, params: []) @current_url = url @current_method = method end # rubocop:disable Metrics/LineLength def add_status_code(status_code, description) check_current_context :resource, :url, :method Utils.deep_set(@resources, "#{@current_resource}.paths.#{@current_url}.actions.#{@current_method}.statuses.#{status_code}", description: description, example: { response: nil }) @current_code = status_code end # rubocop:enable Metrics/LineLength # rubocop:disable Metrics/ParameterLists def add_request_example(url: nil, action: nil, status_code: nil, response: nil, path_params: nil, params: nil) resource = nil @resources.each do |key, res| resource = key if Utils.deep_get(res, "paths.#{url}.actions.#{action}.statuses.#{status_code}") end raise "Resource not found for #{action.upcase} #{url}" unless resource Utils.deep_set(@resources, "#{resource}.paths.#{url}.actions.#{action}.statuses.#{status_code}.example", path_params: path_params, params: params, response: response) end # rubocop:enable Metrics/ParameterLists def to_h { resources: @resources, entities: @entities, } end private # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Style/GuardClause def check_current_context(*scope) scope ||= [] if scope.include?(:resource) raise 'No resource declared' unless @current_resource end if scope.include?(:method) raise 'No action declared' unless @current_method end if scope.include?(:url) raise 'No url declared' unless @current_url end if scope.include?(:code) raise 'No status code declared' unless @current_code end end # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Style/GuardClause def path_param_scope(url_chunks, name) if /:#{name}/ =~ url_chunks[0] :path elsif url_chunks[1] && /:#{name}/ =~ url_chunks[1] :query else raise "#{name} not found in URL #{@current_url}" end end def organize_params(fields) # rubocop:disable Metrics/AbcSize out = { properties: {} } required = [] fields.each do |name, field| allowed_type = %i[array object].include?(field[:type]) || PARAM_TYPES.key?(field[:type]) raise "Field type not allowed: #{field[:type]}" unless allowed_type required.push name.to_s if field[:required] out[:properties][name] = fill_request_param field end out[:required] = required if required.count.positive? out end def fill_request_param(field) if field[:type] && field[:type] == :object organize_params field[:properties] if field[:properties] else { type: field[:type].to_s, description: field[:description] || nil, } end end end end end end