grape-swagger/lib/grape-swagger.rb

345 lines
12 KiB
Ruby
Raw Normal View History

require 'github/markdown'
2012-07-19 16:37:46 +08:00
module Grape
class API
class << self
attr_reader :combined_routes
def add_swagger_documentation(options={})
documentation_class = create_documentation_class
documentation_class.setup({:target_class => self}.merge(options))
2012-07-19 16:37:46 +08:00
mount(documentation_class)
2013-06-18 21:56:15 +08:00
@combined_routes = {}
2014-02-04 07:54:26 +08:00
2013-06-18 21:56:15 +08:00
routes.each do |route|
2013-11-03 22:50:08 +08:00
route_match = route.route_path.split(route.route_prefix).last.match('\/([\w|-]*?)[\.\/\(]')
next if route_match.nil?
resource = route_match.captures.first
2013-06-18 21:56:15 +08:00
next if resource.empty?
resource.downcase!
2014-02-04 07:54:26 +08:00
2013-06-18 21:56:15 +08:00
@combined_routes[resource] ||= []
2013-12-06 08:47:07 +08:00
unless @@hide_documentation_path and route.route_path.include?(@@mount_path)
@combined_routes[resource] << route
end
2013-06-18 21:56:15 +08:00
end
2012-07-19 16:37:46 +08:00
end
private
def create_documentation_class
2012-07-19 16:37:46 +08:00
Class.new(Grape::API) do
class << self
def name
@@class_name
2012-07-19 16:37:46 +08:00
end
end
def self.setup(options)
defaults = {
2013-12-06 08:47:07 +08:00
:target_class => nil,
:mount_path => '/swagger_doc',
:base_path => nil,
:api_version => '0.1',
:markdown => false,
:hide_documentation_path => false,
2013-12-06 08:47:07 +08:00
:hide_format => false,
:models => [],
:info => {},
2013-12-06 08:47:07 +08:00
:authorizations => nil,
2014-02-04 07:54:26 +08:00
:root_base_path => true,
:include_base_url => true
}
2014-02-04 07:54:26 +08:00
options = defaults.merge(options)
2013-11-27 20:35:36 +08:00
target_class = options[:target_class]
@@mount_path = options[:mount_path]
2013-12-06 08:48:26 +08:00
@@class_name = options[:class_name] || options[:mount_path].gsub('/', '')
2013-11-27 20:35:36 +08:00
@@markdown = options[:markdown]
@@hide_format = options[:hide_format]
api_version = options[:api_version]
base_path = options[:base_path]
authorizations = options[:authorizations]
include_base_url = options[:include_base_url]
2013-11-28 04:09:29 +08:00
root_base_path = options[:root_base_path]
2013-11-27 20:43:44 +08:00
extra_info = options[:info]
2013-11-27 18:37:09 +08:00
@@hide_documentation_path = options[:hide_documentation_path]
desc 'Swagger compatible API description'
get @@mount_path do
2013-12-06 08:48:26 +08:00
header['Access-Control-Allow-Origin'] = '*'
header['Access-Control-Request-Method'] = '*'
2014-02-04 07:54:26 +08:00
2013-08-25 06:33:15 +08:00
routes = target_class::combined_routes
if @@hide_documentation_path
routes.reject!{ |route, value| "/#{route}/".index(parse_path(@@mount_path, nil) << '/') == 0 }
end
2013-12-06 08:50:13 +08:00
routes_array = routes.keys.map do |local_route|
next if routes[local_route].all?(&:route_hidden)
2014-02-04 07:54:26 +08:00
parsed_path = route.route_version ? "/#{route.route_version}" : ""
parsed_path += "/#{local_route}"
parsed_path += '.{format}' unless @@hide_format
{
2014-02-04 07:54:26 +08:00
:path => parsed_path,
#:description => "..."
}
2013-12-06 08:50:13 +08:00
end.compact
2013-11-27 18:37:09 +08:00
output = {
apiVersion: api_version,
swaggerVersion: "1.2",
produces: content_types_for(target_class),
2013-12-06 06:46:46 +08:00
operations: [],
apis: routes_array,
info: parse_info(extra_info)
}
2013-11-27 18:37:09 +08:00
2013-12-06 08:50:13 +08:00
basePath = parse_base_path(base_path, request)
2013-11-28 04:09:29 +08:00
output[:basePath] = basePath if basePath && basePath.size > 0 && root_base_path != false
2013-11-27 21:29:40 +08:00
output[:authorizations] = authorizations if authorizations
2013-11-27 18:37:09 +08:00
output
end
2013-12-06 08:50:13 +08:00
desc 'Swagger compatible API description for specific API', :params => {
"name" => {
:desc => "Resource name of mounted API",
:type => "string",
:required => true
}
2013-12-06 08:50:13 +08:00
}
get "#{@@mount_path}/:name" do
2013-11-27 18:37:09 +08:00
header['Access-Control-Allow-Origin'] = '*'
header['Access-Control-Request-Method'] = '*'
2014-02-04 07:54:26 +08:00
models = []
2013-08-25 06:33:15 +08:00
routes = target_class::combined_routes[params[:name]]
2014-02-04 07:54:26 +08:00
ops = routes.reject(&:route_hidden).group_by do |route|
parse_path(route.route_path, api_version)
end
apis = []
ops.each do |path, routes|
operations = routes.map do |route|
notes = as_markdown(route.route_notes)
http_codes = parse_http_codes(route.route_http_codes)
models << route.route_entity if route.route_entity
operation = {
:produces => content_types_for(target_class),
:notes => notes.to_s,
:summary => route.route_description || '',
:nickname => route.route_nickname || (route.route_method + route.route_path.gsub(/[\/:\(\)\.]/,'-')),
:httpMethod => route.route_method,
:parameters => parse_header_params(route.route_headers) +
parse_params(route.route_params, route.route_path, route.route_method)
}
operation.merge!(:type => parse_entity_name(route.route_entity)) if route.route_entity
operation.merge!(:responseMessages => http_codes) unless http_codes.empty?
operation
end.compact
apis << {
path: path,
operations: operations
}
2014-02-04 07:54:26 +08:00
end
api_description = {
apiVersion: api_version,
2013-11-27 18:37:09 +08:00
swaggerVersion: "1.2",
resourcePath: "",
2014-02-04 07:54:26 +08:00
apis: apis
}
2013-11-27 21:29:40 +08:00
basePath = parse_base_path(base_path, request)
api_description[:basePath] = basePath if basePath && basePath.size > 0
api_description[:models] = parse_entity_models(models) unless models.empty?
2014-02-04 07:54:26 +08:00
api_description
end
2012-07-19 16:37:46 +08:00
end
helpers do
def as_markdown(description)
description && @@markdown ? GitHub::Markdown.render_gfm(strip_heredoc(description)) : description
end
def parse_params(params, path, method)
2013-12-06 08:48:26 +08:00
params ||= []
2014-02-04 07:54:26 +08:00
2013-12-06 08:48:26 +08:00
params.map do |param, value|
value[:type] = 'file' if value.is_a?(Hash) && value[:type] == 'Rack::Multipart::UploadedFile'
dataType = value.is_a?(Hash) ? (value[:type] || 'String').to_s : 'String'
description = value.is_a?(Hash) ? value[:desc] || value[:description] : ''
required = value.is_a?(Hash) ? !!value[:required] : false
defaultValue = value.is_a?(Hash) ? value[:defaultValue] : nil
paramType = if path.include?(":#{param}")
'path'
else
%w[ POST PUT PATCH ].include?(method) ? 'form' : 'query'
end
2013-12-06 08:48:26 +08:00
name = (value.is_a?(Hash) && value[:full_name]) || param
2014-02-04 07:54:26 +08:00
parsed_params = {
2013-12-06 08:48:26 +08:00
paramType: paramType,
name: name,
description: as_markdown(description),
type: dataType,
2014-01-09 20:48:32 +08:00
dataType: dataType,
required: required
2013-12-06 08:48:26 +08:00
}
parsed_params.merge!({defaultValue: defaultValue}) if defaultValue
parsed_params
end
end
2014-02-04 07:54:26 +08:00
def content_types_for(target_class)
content_types = (target_class.settings[:content_types] || {}).values
2014-02-04 07:54:26 +08:00
if content_types.empty?
formats = [target_class.settings[:format], target_class.settings[:default_format]].compact.uniq
formats = Grape::Formatter::Base.formatters({}).keys if formats.empty?
content_types = Grape::ContentTypes::CONTENT_TYPES.select{|content_type, mime_type| formats.include? content_type}.values
end
2014-02-04 07:54:26 +08:00
content_types.uniq
end
def parse_info(info)
{
contact: info[:contact],
description: info[:description],
license: info[:license],
licenseUrl: info[:license_url],
termsOfServiceUrl: info[:terms_of_service_url],
title: info[:title]
}.delete_if{|_, value| value.blank?}
end
def parse_header_params(params)
2013-12-06 08:48:26 +08:00
params ||= []
2014-02-04 07:54:26 +08:00
2013-12-06 08:48:26 +08:00
params.map do |param, value|
dataType = 'String'
description = value.is_a?(Hash) ? value[:description] : ''
required = value.is_a?(Hash) ? !!value[:required] : false
defaultValue = value.is_a?(Hash) ? value[:defaultValue] : nil
2013-12-06 08:48:26 +08:00
paramType = "header"
2014-02-04 07:54:26 +08:00
parsed_params = {
2013-12-06 08:48:26 +08:00
paramType: paramType,
name: param,
description: as_markdown(description),
type: dataType,
dataType: dataType,
required: required
2013-12-06 08:48:26 +08:00
}
parsed_params.merge!({defaultValue: defaultValue}) if defaultValue
parsed_params
2012-07-19 16:37:46 +08:00
end
end
2012-08-17 00:07:00 +08:00
def parse_path(path, version)
2012-07-19 16:37:46 +08:00
# adapt format to swagger format
2013-12-06 08:48:26 +08:00
parsed_path = path.gsub('(.:format)', @@hide_format ? '' : '.{format}')
# This is attempting to emulate the behavior of
# Rack::Mount::Strexp. We cannot use Strexp directly because
# all it does is generate regular expressions for parsing URLs.
# TODO: Implement a Racc tokenizer to properly generate the
# parsed path.
parsed_path = parsed_path.gsub(/:([a-zA-Z_]\w*)/, '{\1}')
2012-08-17 00:07:00 +08:00
# add the version
version ? parsed_path.gsub('{version}', version) : parsed_path
2012-07-19 16:37:46 +08:00
end
2014-02-04 09:15:23 +08:00
def parse_entity_name(name)
entity_parts = name.to_s.split('::')
entity_parts.reject! {|p| p == "Entity" || p == "Entities"}
entity_parts.join("::")
end
def parse_entity_models(models)
result = {}
2014-02-04 07:54:26 +08:00
2013-12-06 09:50:47 +08:00
models.each do |model|
2014-02-04 09:15:23 +08:00
name = parse_entity_name(model)
2013-12-06 09:50:47 +08:00
properties = {}
2014-02-04 07:54:26 +08:00
2013-12-06 09:50:47 +08:00
model.documentation.each do |property_name, property_info|
properties[property_name] = property_info
2014-02-04 07:54:26 +08:00
2013-12-06 09:50:47 +08:00
# rename Grape Entity's "desc" to "description"
if property_description = property_info.delete(:desc)
property_info[:description] = property_description
end
end
2014-02-04 07:54:26 +08:00
result[name] = {
2013-12-06 08:48:26 +08:00
id: model.instance_variable_get(:@root) || name,
name: model.instance_variable_get(:@root) || name,
properties: properties
}
end
2014-02-04 07:54:26 +08:00
result
end
def parse_http_codes codes
codes ||= {}
codes.map do |k, v|
{
code: k,
message: v,
#responseModel: ...
}
end
end
2013-12-06 08:48:26 +08:00
def try(*args, &block)
if args.empty? && block_given?
yield self
2013-12-06 08:48:26 +08:00
elsif respond_to?(args.first)
public_send(*args, &block)
end
end
def strip_heredoc(string)
indent = string.scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
string.gsub(/^[ \t]{#{indent}}/, '')
end
2013-06-19 16:35:50 +08:00
def parse_base_path(base_path, request)
if base_path.is_a?(Proc)
base_path.call(request)
elsif base_path.is_a?(String)
URI(base_path).relative? ? URI.join(request.base_url, base_path).to_s : base_path
else
request.base_url
end
2013-06-19 16:35:50 +08:00
end
2012-07-19 16:37:46 +08:00
end
end
end
end
end
end