archiloque/sinatra-swagger-exposer

View on GitHub
lib/sinatra/swagger-exposer/swagger-exposer.rb

Summary

Maintainability
B
5 hrs
Test Coverage
require 'sinatra/base'
require 'json'

require_relative 'configuration/swagger-endpoint'
require_relative 'configuration/swagger-endpoint-parameter'
require_relative 'configuration/swagger-endpoint-response'
require_relative 'configuration/swagger-response-headers'
require_relative 'configuration/swagger-info'
require_relative 'configuration/swagger-types'
require_relative 'swagger-content-creator'
require_relative 'swagger-invalid-exception'
require_relative 'swagger-request-processor-creator'

module Sinatra

  # Expose swagger API from your Sinatra app
  module SwaggerExposer

    # Called when we register the extension
    # @param app [Sinatra::Base]
    def self.registered(app)
      app.set :result_validation, (ENV['RACK_ENV'] != 'production')
      app.set :swagger_endpoints, []
      app.set :swagger_current_endpoint_info, {}
      app.set :swagger_current_endpoint_parameters, {}
      app.set :swagger_current_endpoint_responses, {}

      swagger_types = Sinatra::SwaggerExposer::Configuration::SwaggerTypes.new
      app.set :swagger_types, swagger_types

      response_headers = Sinatra::SwaggerExposer::Configuration::SwaggerResponseHeaders.new
      app.set :swagger_response_headers, response_headers

      app.set :swagger_processor_creator, Sinatra::SwaggerExposer::SwaggerProcessorCreator.new(swagger_types)
      declare_swagger_endpoint(app)
    end

    # Declare the endpoint to serve the swagger content
    # @param app [Sinatra::Base]
    def self.declare_swagger_endpoint(app)
      app.endpoint_summary 'The swagger endpoint'
      app.endpoint_tags 'swagger'
      app.endpoint_response 200
      app.get('/swagger_doc.json') do
        swagger_content = Sinatra::SwaggerExposer::SwaggerContentCreator.new(
          settings.respond_to?(:swagger_info) ? settings.swagger_info : nil,
          settings.swagger_types,
          settings.swagger_endpoints
        ).to_swagger
        content_type :json
        swagger_content.to_json
      end

      app.endpoint_summary 'Option method for the swagger endpoint, useful for some CORS stuff'
      app.endpoint_tags 'swagger'
      app.endpoint_response 200
      app.endpoint_produces 'text/plain;charset=utf-8'
      app.options('/swagger_doc.json') do
        content_type :text
        200
      end
    end

    # Provide a summary for the endpoint
    # @param summary [String]
    def endpoint_summary(summary)
      set_if_type_and_not_exist(summary, :summary, String)
    end

    # Provide a path
    # @param path [String]
    def endpoint_path(path)
      set_if_type_and_not_exist(path, :path, String)
    end

    # Provide a description for the endpoint
    # @param description [String]
    def endpoint_description(description)
      set_if_type_and_not_exist(description, :description, String)
    end

    # Provide tags for the endpoint
    # @param tags [Array<String>]
    def endpoint_tags(*tags)
      set_if_not_exist(tags, :tags)
    end

    # Provide produces params for the endpoint
    # @param produces [Array<String>] the response types
    def endpoint_produces(*produces)
      set_if_not_exist(produces, :produces)
    end

    # Define parameter for the endpoint
    def endpoint_parameter(name, description, how_to_pass, required, type, params = {})
      parameters = settings.swagger_current_endpoint_parameters
      check_if_not_duplicate(name, parameters, 'Parameter')
      parameters[name] = Sinatra::SwaggerExposer::Configuration::SwaggerEndpointParameter.new(
        name,
        description,
        how_to_pass,
        required,
        type,
        params,
        settings.swagger_types.types_names)
    end

    # Define fluent endpoint dispatcher
    # @param params [Hash] the parameters
    def endpoint(params)
      params.each_pair do |param_name, param_value|
        case param_name
          when :summary
            endpoint_summary param_value
          when :description
            endpoint_description param_value
          when :tags
            endpoint_tags *param_value
          when :produces
            endpoint_produces *param_value
          when :path
            endpoint_path param_value
          when :parameters
            param_value.each do |param, args_param|
              endpoint_parameter param, *args_param
            end
          when :responses
            param_value.each do |code, args_response|
              endpoint_response code, *args_response
            end
          else
            raise SwaggerInvalidException.new("Invalid endpoint parameter [#{param_name}]")
        end
      end
    end

    # General information
    def general_info(params)
      set :swagger_info, Sinatra::SwaggerExposer::Configuration::SwaggerInfo.new(params)
    end

    # Declare a type
    def type(name, params)
      settings.swagger_types.add_type(name, params)
    end

    # Declare a response header
    def response_header(name, type, description)
      settings.swagger_response_headers.add_response_header(name, type, description)
    end

    # Declare a response
    # @param code [Integer] the response code
    # @param type the type
    # @param description [String] the description
    # @param headers [Array<String>] the headers names
    def endpoint_response(code, type = nil, description = nil, headers = [])
      responses = settings.swagger_current_endpoint_responses
      check_if_not_duplicate(code, responses, 'Response')
      responses[code] = Sinatra::SwaggerExposer::Configuration::SwaggerEndpointResponse.new(
        type,
        description,
        settings.swagger_types.types_names,
        headers,
        settings.swagger_response_headers
      )
    end

    # Override Sinatra route method
    def route(verb, path, options = {}, &block)
      no_swagger = options[:no_swagger]
      options.delete(:no_swagger)
      if (verb == 'HEAD') || no_swagger
        super(verb, path, options, &block)
      else
        request_processor = create_request_processor(verb.downcase, path, options)
        super(verb, path, options) do |*params|
          response = catch(:halt) do
            request_processor.run(self, params, &block)
          end
          if settings.result_validation
            begin
              # Inspired from Sinatra#invoke
              if (Fixnum === response) or (String === response)
                response = [response]
              end
              if (Array === response) and (Fixnum === response.first)
                response_for_validation = response.dup
                response_status = response_for_validation.shift
                response_body = response_for_validation.pop
                response_headers = (response_for_validation.pop || {}).merge(self.response.header)
                response_content_type = response_headers['Content-Type']
                request_processor.validate_response(response_body, response_content_type, response_status)
              elsif response.respond_to? :each
                request_processor.validate_response(response.first.dup, self.response.header['Content-Type'], 200)
              end
            rescue Sinatra::SwaggerExposer::SwaggerInvalidException => e
              content_type :json
              throw :halt, [400, {:code => 400, :message => e.message}.to_json]
            end
          end
          throw :halt, response
        end
      end
    end

    private

    # Call for each endpoint declaration
    # @return [Sinatra::SwaggerExposer::Processing::SwaggerRequestProcessor]
    def create_request_processor(type, path, opts)
      current_endpoint_info = settings.swagger_current_endpoint_info
      current_endpoint_parameters = settings.swagger_current_endpoint_parameters
      current_endpoint_responses = settings.swagger_current_endpoint_responses
      endpoint = Sinatra::SwaggerExposer::Configuration::SwaggerEndpoint.new(
        type,
        path,
        current_endpoint_parameters.values,
        current_endpoint_responses.clone,
        current_endpoint_info[:summary],
        current_endpoint_info[:description],
        current_endpoint_info[:tags],
        current_endpoint_info[:path],
        current_endpoint_info[:produces])
      settings.swagger_endpoints << endpoint
      current_endpoint_info.clear
      current_endpoint_parameters.clear
      current_endpoint_responses.clear
      settings.swagger_processor_creator.create_request_processor(endpoint)
    end

    def set_if_not_exist(value, name)
      if settings.swagger_current_endpoint_info.key? name
        raise SwaggerInvalidException.new("#{name} with value [#{value}] already defined: [#{settings.swagger_current_endpoint_info[name]}]")
      end
      settings.swagger_current_endpoint_info[name] = value
    end

    def set_if_type_and_not_exist(value, name, type)
      unless value.is_a? type
        raise SwaggerInvalidException.new("#{name} [#{value}] should be a #{type.to_s.downcase}")
      end
      set_if_not_exist(value, name)
    end

    # Check if a value does not exist in a hash and throw an Exception
    # @param key the key to validate
    # @param values [Hash] the existing keys
    # @param name [String] the valud name for the error message
    def check_if_not_duplicate(key, values, name)
      if values.key? key
        raise SwaggerInvalidException.new("#{name} already exist for #{key} with value [#{values[key]}]")
      end
    end

  end

end