# frozen_string_literal: true require 'rspec/rails/api/utils' require 'active_support' module RSpec module Rails module Api # Class to render metadatas. # Example: # ```rb # renderer = RSpec::Rails::Api::OpenApiRenderer.new # renderer.merge_context(example_context) # renderer.write_files # ``` class OpenApiRenderer # rubocop:disable Metrics/ClassLength attr_writer :api_servers, :api_title, :api_version, :api_description, :api_tos def initialize @metadata = { resources: {}, entities: {} } @api_infos = {} @api_servers = {} @api_paths = {} @api_components = {} @api_tags = [] @api_contact = {} @api_license = {} end def merge_context(context) @metadata[:resources].deep_merge! context[:resources] @metadata[:entities].deep_merge! context[:entities] # Save context to make the fixture (will be saved in the reference project) # File.write ::Rails.root.join('tmp', 'meta.yaml'), context.to_yaml end def write_files(path = nil, only: %i[yaml json]) content = prepare_metadata path ||= ::Rails.root.join('tmp', 'rspec_api_rails') only.each do |type| next unless %i[yaml json].include? type File.write "#{path}.#{type}", content.send("to_#{type}") end end def prepare_metadata # Example: https://github.com/OAI/OpenAPI-Specification/blob/master/examples/v3.0/petstore-expanded.yaml extract_metadatas { openapi: '3.0.0', info: @api_infos, servers: @api_servers, paths: @api_paths, components: @api_components, tags: @api_tags, }.deep_stringify_keys end def api_contact=(name: nil, email: nil, url: nil) @api_contact[:name] = name if name @api_contact[:email] = email if email @api_contact[:url] = url if url end def api_license=(name: nil, url: nil) @api_license[:name] = name if name @api_license[:url] = url if url end private def extract_metadatas extract_from_resources api_infos api_servers end def extract_from_resources @metadata[:resources].each do |resource_key, resource| @api_tags.push( name: resource_key.to_s, description: resource[:description] ) process_resource resource: resource_key, resource_config: resource end end def process_resource(resource: nil, resource_config: nil) # rubocop:disable Metrics/MethodLength resource_config[:paths].each do |path_key, path| url = path_with_params path_key.to_s actions = {} parameters = path.key?(:path_params) ? process_path_params(path[:path_params]) : [] path[:actions].each_key do |action| next unless %i[get post put patch delete].include? action actions[action] = process_action resource: resource, path: path_key, path_config: path, action_config: action, parameters: parameters end @api_paths[url] = actions end end def process_path_params(params) parameters = [] params.each do |name, param| parameters.push process_path_param name, param end parameters end def process_path_param(name, param) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize parameter = { name: name.to_s, description: param[:description], required: param[:required] || true, in: param[:scope].to_s, schema: { type: PARAM_TYPES[param[:type]][:type], }, } parameter[:schema][:format] = PARAM_TYPES[param[:type]][:format] if PARAM_TYPES[param[:type]][:format] parameter end # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def process_action(resource: nil, path: nil, path_config: nil, action_config: nil, parameters: nil) responses = {} request_body = nil if %i[post put patch].include? action_config if path_config[:actions][action_config][:params].keys.count.positive? schema = path_config[:actions][action_config][:params] schema_ref = escape_operation_id("#{action_config}_#{path}") request_body = process_request_body schema: schema, ref: schema_ref end end path_config[:actions][action_config][:statuses].each do |status_key, status| content = status[:example][:response] responses[status_key] = process_response status: status_key, status_config: status, content: content end description = path_config[:actions][action_config][:description] action = { description: description, operationId: "#{resource} #{description}".downcase.gsub(/[^\w]/, '_'), parameters: parameters, responses: responses, tags: [resource.to_s], } action[:requestBody] = request_body if request_body action end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength def process_request_body(schema: nil, ref: nil) Utils.deep_set @api_components, "schemas.#{ref}", schema { # description: '', required: true, content: { 'application/json' => { schema: { '$ref' => "#/components/schemas/#{ref}" }, }, }, } end def process_response(status: nil, status_config: nil, content: nil) response = { description: status_config[:description], } return response unless status.to_s != '204' && content # No content response[:content] = { 'application/json': { examples: { default: { value: JSON.pretty_generate(JSON.parse(content)) } }, }, } response end def path_with_params(string) string.gsub(/(?::(\w*))/) do |e| "{#{e.sub(':', '')}}" end end def escape_operation_id(string) string.downcase.gsub(/[^\w]+/, '_') end def api_infos # rubocop:disable Metrics/CyclomaticComplexity @api_infos = { title: @api_title || 'Some sample app', version: @api_version || '1.0', } @api_infos[:description] = @api_description if @api_description @api_infos[:termsOfService] = @api_tos if @api_tos @api_infos[:contact] = @api_contact if @api_contact[:name] @api_infos[:license] = @api_license if @api_license[:name] @api_infos end def api_servers @api_servers || [ { url: 'http://api.example.com' }, ] end end end end end