2012-08-02 16:11:37 +08:00
|
|
|
require 'kramdown'
|
|
|
|
|
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
|
|
|
|
|
2012-07-19 22:15:14 +08:00
|
|
|
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 = {}
|
2013-12-06 08:47:07 +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!
|
2013-12-06 08:47:07 +08:00
|
|
|
|
2013-06-18 21:56:15 +08:00
|
|
|
@combined_routes[resource] ||= []
|
2013-08-10 05:56:19 +08:00
|
|
|
|
2013-12-06 08:47:07 +08:00
|
|
|
unless @@hide_documentation_path and route.route_path.include?(@@mount_path)
|
2013-08-10 05:56:19 +08:00
|
|
|
@combined_routes[resource] << route
|
|
|
|
end
|
2013-06-18 21:56:15 +08:00
|
|
|
end
|
|
|
|
|
2012-07-19 16:37:46 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2012-07-19 22:15:14 +08:00
|
|
|
def create_documentation_class
|
2012-07-24 22:47:58 +08:00
|
|
|
|
2012-07-19 16:37:46 +08:00
|
|
|
Class.new(Grape::API) do
|
|
|
|
class << self
|
|
|
|
def name
|
2012-07-19 22:15:14 +08:00
|
|
|
@@class_name
|
2012-07-19 16:37:46 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-07-19 22:15:14 +08:00
|
|
|
def self.setup(options)
|
|
|
|
defaults = {
|
2013-12-06 08:47:07 +08:00
|
|
|
:models => [],
|
|
|
|
:target_class => nil,
|
|
|
|
:mount_path => '/swagger_doc',
|
|
|
|
:base_path => nil,
|
|
|
|
:api_version => '0.1',
|
|
|
|
:markdown => false,
|
|
|
|
:hide_format => false,
|
|
|
|
:authorizations => nil,
|
|
|
|
:root_base_path => true,
|
|
|
|
:include_base_url => true,
|
|
|
|
:hide_documentation_path => false
|
2012-07-19 22:15:14 +08:00
|
|
|
}
|
2013-11-28 04:09:29 +08:00
|
|
|
|
2012-07-19 22:15:14 +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]
|
2012-07-19 22:15:14 +08:00
|
|
|
|
|
|
|
desc 'Swagger compatible API description'
|
|
|
|
get @@mount_path do
|
2013-12-06 08:48:26 +08:00
|
|
|
header['Access-Control-Allow-Origin'] = '*'
|
2012-07-19 22:15:14 +08:00
|
|
|
header['Access-Control-Request-Method'] = '*'
|
2013-12-06 08:48:26 +08:00
|
|
|
|
2013-08-25 06:33:15 +08:00
|
|
|
routes = target_class::combined_routes
|
2012-07-19 22:15:14 +08:00
|
|
|
|
2012-10-19 22:49:43 +08:00
|
|
|
if @@hide_documentation_path
|
|
|
|
routes.reject!{ |route, value| "/#{route}/".index(parse_path(@@mount_path, nil) << '/') == 0 }
|
|
|
|
end
|
|
|
|
|
2013-09-25 01:10:55 +08:00
|
|
|
routes_array = routes.keys.map { |local_route|
|
2013-09-25 19:35:16 +08:00
|
|
|
next if routes[local_route].all? { |route| route.route_hidden }
|
2013-11-27 20:35:36 +08:00
|
|
|
{ :path => "#{include_base_url ? parse_path(route.route_path.gsub('(.:format)', ''),route.route_version) : ''}/#{local_route}#{@@hide_format ? '' : '.{format}'}" }
|
2013-09-25 01:10:55 +08:00
|
|
|
}.compact
|
2013-06-21 05:32:37 +08:00
|
|
|
|
2013-11-27 18:37:09 +08:00
|
|
|
output = {
|
|
|
|
apiVersion: api_version,
|
|
|
|
swaggerVersion: "1.2",
|
2013-12-06 06:46:46 +08:00
|
|
|
operations: [],
|
2013-11-27 18:37:09 +08:00
|
|
|
apis: routes_array
|
2012-07-19 22:15:14 +08:00
|
|
|
}
|
2013-11-27 18:37:09 +08:00
|
|
|
|
2013-11-27 21:29:40 +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
|
|
|
|
output[:info] = extra_info if extra_info
|
2013-11-27 18:37:09 +08:00
|
|
|
|
|
|
|
output
|
2012-07-19 22:15:14 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
desc 'Swagger compatible API description for specific API', :params =>
|
|
|
|
{
|
2012-08-02 16:11:37 +08:00
|
|
|
"name" => { :desc => "Resource name of mounted API", :type => "string", :required => true },
|
2012-07-19 22:15:14 +08:00
|
|
|
}
|
|
|
|
get "#{@@mount_path}/:name" do
|
2013-11-27 18:37:09 +08:00
|
|
|
header['Access-Control-Allow-Origin'] = '*'
|
2012-07-19 22:15:14 +08:00
|
|
|
header['Access-Control-Request-Method'] = '*'
|
2013-07-27 03:12:20 +08:00
|
|
|
models = []
|
2013-08-25 06:33:15 +08:00
|
|
|
routes = target_class::combined_routes[params[:name]]
|
|
|
|
routes_array = routes.map {|route|
|
2013-09-25 19:35:16 +08:00
|
|
|
next if route.route_hidden
|
2013-10-17 03:43:05 +08:00
|
|
|
notes = as_markdown(route.route_notes)
|
2013-01-24 12:17:04 +08:00
|
|
|
http_codes = parse_http_codes route.route_http_codes
|
2013-07-27 03:12:20 +08:00
|
|
|
models << route.route_entity if route.route_entity
|
2013-01-24 12:17:04 +08:00
|
|
|
operations = {
|
2012-08-02 16:11:37 +08:00
|
|
|
:notes => notes,
|
2012-07-19 22:15:14 +08:00
|
|
|
:summary => route.route_description || '',
|
2013-11-27 22:22:53 +08:00
|
|
|
:nickname => (route.route_nickname || (route.route_method + route.route_path.gsub(/[\/:\(\)\.]/,'-'))),
|
2012-07-19 22:15:14 +08:00
|
|
|
:httpMethod => route.route_method,
|
2012-08-17 03:47:18 +08:00
|
|
|
:parameters => parse_header_params(route.route_headers) +
|
2013-07-27 03:41:03 +08:00
|
|
|
parse_params(route.route_params, route.route_path, route.route_method)
|
2013-01-24 12:17:04 +08:00
|
|
|
}
|
2013-11-27 21:59:04 +08:00
|
|
|
operations.merge!({:type => route.route_entity.to_s.split('::')[-1]}) if route.route_entity
|
2013-01-24 12:17:04 +08:00
|
|
|
operations.merge!({:errorResponses => http_codes}) unless http_codes.empty?
|
|
|
|
{
|
|
|
|
:path => parse_path(route.route_path, api_version),
|
|
|
|
:operations => [operations]
|
2012-07-19 22:15:14 +08:00
|
|
|
}
|
2013-09-25 19:35:16 +08:00
|
|
|
}.compact
|
2012-07-19 22:15:14 +08:00
|
|
|
|
2013-07-27 03:12:20 +08:00
|
|
|
api_description = {
|
2012-07-25 22:47:36 +08:00
|
|
|
apiVersion: api_version,
|
2013-11-27 18:37:09 +08:00
|
|
|
swaggerVersion: "1.2",
|
2012-07-19 22:15:14 +08:00
|
|
|
resourcePath: "",
|
|
|
|
apis: routes_array
|
|
|
|
}
|
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?
|
|
|
|
|
2013-07-27 03:12:20 +08:00
|
|
|
api_description
|
2012-07-19 22:15:14 +08:00
|
|
|
end
|
2012-07-19 16:37:46 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
helpers do
|
2013-10-17 03:43:05 +08:00
|
|
|
|
|
|
|
def as_markdown(description)
|
|
|
|
description && @@markdown ? Kramdown::Document.new(strip_heredoc(description)).to_html : description
|
|
|
|
end
|
|
|
|
|
2012-07-26 20:41:47 +08:00
|
|
|
def parse_params(params, path, method)
|
2013-12-06 08:48:26 +08:00
|
|
|
params ||= []
|
|
|
|
|
|
|
|
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
|
|
|
|
paramType = path.include?(":#{param}") ? 'path' : (method == 'POST') ? 'form' : 'query'
|
|
|
|
name = (value.is_a?(Hash) && value[:full_name]) || param
|
|
|
|
|
|
|
|
{
|
|
|
|
paramType: paramType,
|
|
|
|
name: name,
|
|
|
|
description: as_markdown(description),
|
|
|
|
type: dataType,
|
|
|
|
required: required
|
|
|
|
}
|
2012-08-17 03:47:18 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse_header_params(params)
|
2013-12-06 08:48:26 +08:00
|
|
|
params ||= []
|
|
|
|
|
|
|
|
params.map do |param, value|
|
|
|
|
dataType = 'String'
|
|
|
|
description = value.is_a?(Hash) ? value[:description] : ''
|
|
|
|
required = value.is_a?(Hash) ? !!value[:required] : false
|
|
|
|
paramType = "header"
|
|
|
|
|
|
|
|
{
|
|
|
|
paramType: paramType,
|
|
|
|
name: param,
|
|
|
|
description: as_markdown(description),
|
|
|
|
type: dataType,
|
|
|
|
required: required
|
|
|
|
}
|
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}')
|
2013-01-18 23:20:56 +08:00
|
|
|
# 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
|
2012-10-17 00:12:59 +08:00
|
|
|
# parsed path.
|
|
|
|
parsed_path = parsed_path.gsub(/:([a-zA-Z_]\w*)/, '{\1}')
|
2012-08-17 00:07:00 +08:00
|
|
|
# add the version
|
2013-04-28 11:12:45 +08:00
|
|
|
version ? parsed_path.gsub('{version}', version) : parsed_path
|
2012-07-19 16:37:46 +08:00
|
|
|
end
|
2013-04-28 11:12:45 +08:00
|
|
|
|
2013-07-27 03:12:20 +08:00
|
|
|
def parse_entity_models(models)
|
|
|
|
result = {}
|
|
|
|
models.each do |model|
|
|
|
|
name = model.to_s.split('::')[-1]
|
|
|
|
result[name] = {
|
2013-12-06 08:48:26 +08:00
|
|
|
id: model.instance_variable_get(:@root) || name,
|
|
|
|
name: model.instance_variable_get(:@root) || name,
|
2013-07-27 03:12:20 +08:00
|
|
|
properties: model.documentation
|
|
|
|
}
|
|
|
|
end
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
2013-01-24 12:17:04 +08:00
|
|
|
def parse_http_codes codes
|
|
|
|
codes ||= {}
|
|
|
|
codes.collect do |k, v|
|
2013-10-18 01:49:47 +08:00
|
|
|
{ code: k, message: v }
|
2013-01-24 12:17:04 +08:00
|
|
|
end
|
|
|
|
end
|
2013-04-04 07:34:09 +08:00
|
|
|
|
2013-12-06 08:48:26 +08:00
|
|
|
def try(*args, &block)
|
|
|
|
if args.empty? && block_given?
|
2013-04-04 07:34:09 +08:00
|
|
|
yield self
|
2013-12-06 08:48:26 +08:00
|
|
|
elsif respond_to?(args.first)
|
|
|
|
public_send(*args, &block)
|
2013-04-04 07:34:09 +08:00
|
|
|
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)
|
|
|
|
(base_path.is_a?(Proc) ? base_path.call(request) : base_path) || request.base_url
|
|
|
|
end
|
2012-07-19 16:37:46 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|