2015-02-05 06:12:48 +08:00
|
|
|
require 'grape'
|
2014-07-14 21:36:45 +08:00
|
|
|
require 'grape-swagger/version'
|
2014-08-13 17:43:48 +08:00
|
|
|
require 'grape-swagger/errors'
|
|
|
|
require 'grape-swagger/markdown'
|
|
|
|
require 'grape-swagger/markdown/kramdown_adapter'
|
|
|
|
require 'grape-swagger/markdown/redcarpet_adapter'
|
2012-08-02 16:11:37 +08:00
|
|
|
|
2012-07-19 16:37:46 +08:00
|
|
|
module Grape
|
|
|
|
class API
|
|
|
|
class << self
|
2015-04-29 03:54:23 +08:00
|
|
|
attr_accessor :combined_routes, :combined_namespaces, :combined_namespace_routes, :combined_namespace_identifiers
|
2012-07-19 16:37:46 +08:00
|
|
|
|
2014-07-14 21:59:11 +08:00
|
|
|
def add_swagger_documentation(options = {})
|
2012-07-19 16:37:46 +08:00
|
|
|
documentation_class = create_documentation_class
|
|
|
|
|
2015-04-29 03:54:23 +08:00
|
|
|
options = { target_class: self }.merge(options)
|
|
|
|
@target_class = options[:target_class]
|
|
|
|
|
|
|
|
documentation_class.setup(options)
|
2012-07-19 16:37:46 +08:00
|
|
|
mount(documentation_class)
|
2013-06-18 21:56:15 +08:00
|
|
|
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_routes = {}
|
|
|
|
@target_class.routes.each do |route|
|
2014-11-30 02:39:08 +08:00
|
|
|
route_path = route.route_path
|
|
|
|
route_match = route_path.split(/^.*?#{route.route_prefix.to_s}/).last
|
|
|
|
next unless route_match
|
2015-01-21 05:40:52 +08:00
|
|
|
route_match = route_match.match('\/([\w|-]*?)[\.\/\(]') || route_match.match('\/([\w|-]*)$')
|
2014-11-30 02:39:08 +08:00
|
|
|
next unless route_match
|
2013-11-03 22:50:08 +08:00
|
|
|
resource = route_match.captures.first
|
2013-06-18 21:56:15 +08:00
|
|
|
next if resource.empty?
|
|
|
|
resource.downcase!
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_routes[resource] ||= []
|
2014-12-01 23:40:09 +08:00
|
|
|
next if documentation_class.hide_documentation_path && route.route_path.include?(documentation_class.mount_path)
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_routes[resource] << route
|
2013-06-18 21:56:15 +08:00
|
|
|
end
|
2014-07-14 23:26:27 +08:00
|
|
|
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_namespaces = {}
|
|
|
|
combine_namespaces(@target_class)
|
2015-02-26 00:42:32 +08:00
|
|
|
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_namespace_routes = {}
|
|
|
|
@target_class.combined_namespace_identifiers = {}
|
|
|
|
combine_namespace_routes(@target_class.combined_namespaces)
|
2015-02-26 00:42:32 +08:00
|
|
|
|
2015-04-29 03:54:23 +08:00
|
|
|
exclusive_route_keys = @target_class.combined_routes.keys - @target_class.combined_namespaces.keys
|
|
|
|
exclusive_route_keys.each { |key| @target_class.combined_namespace_routes[key] = @target_class.combined_routes[key] }
|
2014-12-01 21:59:20 +08:00
|
|
|
documentation_class
|
2014-08-03 08:01:14 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def combine_namespaces(app)
|
|
|
|
app.endpoints.each do |endpoint|
|
2014-11-11 04:08:52 +08:00
|
|
|
ns = if endpoint.respond_to?(:namespace_stackable)
|
|
|
|
endpoint.namespace_stackable(:namespace).last
|
|
|
|
else
|
|
|
|
endpoint.settings.stack.last[:namespace]
|
|
|
|
end
|
2015-02-26 00:42:32 +08:00
|
|
|
# use the full namespace here (not the latest level only)
|
|
|
|
# and strip leading slash
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_namespaces[endpoint.namespace.sub(/^\//, '')] = ns if ns
|
2014-08-03 08:01:14 +08:00
|
|
|
|
|
|
|
combine_namespaces(endpoint.options[:app]) if endpoint.options[:app]
|
2014-07-14 23:26:27 +08:00
|
|
|
end
|
2012-07-19 16:37:46 +08:00
|
|
|
end
|
|
|
|
|
2015-02-26 00:42:32 +08:00
|
|
|
def combine_namespace_routes(namespaces)
|
|
|
|
# iterate over each single namespace
|
|
|
|
namespaces.each do |name, namespace|
|
|
|
|
# get the parent route for the namespace
|
|
|
|
parent_route_name = name.match(%r{^/?([^/]*).*$})[1]
|
2015-04-29 03:54:23 +08:00
|
|
|
parent_route = @target_class.combined_routes[parent_route_name]
|
2015-02-26 00:42:32 +08:00
|
|
|
# fetch all routes that are within the current namespace
|
|
|
|
namespace_routes = parent_route.collect do |route|
|
2015-03-11 21:28:36 +08:00
|
|
|
route if (route.route_path.start_with?(route.route_prefix ? "/#{route.route_prefix}/#{name}" : "/#{name}") || route.route_path.start_with?((route.route_prefix ? "/#{route.route_prefix}/:version/#{name}" : "/:version/#{name}"))) &&
|
2015-02-26 00:42:32 +08:00
|
|
|
(route.instance_variable_get(:@options)[:namespace] == "/#{name}" || route.instance_variable_get(:@options)[:namespace] == "/:version/#{name}")
|
|
|
|
end.compact
|
|
|
|
|
|
|
|
if namespace.options.key?(:swagger) && namespace.options[:swagger][:nested] == false
|
|
|
|
# Namespace shall appear as standalone resource, use specified name or use normalized path as name
|
|
|
|
if namespace.options[:swagger].key?(:name)
|
|
|
|
identifier = namespace.options[:swagger][:name].gsub(/ /, '-')
|
|
|
|
else
|
|
|
|
identifier = name.gsub(/_/, '-').gsub(/\//, '_')
|
|
|
|
end
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_namespace_identifiers[identifier] = name
|
|
|
|
@target_class.combined_namespace_routes[identifier] = namespace_routes
|
2015-02-26 00:42:32 +08:00
|
|
|
|
|
|
|
# get all nested namespaces below the current namespace
|
|
|
|
sub_namespaces = standalone_sub_namespaces(name, namespaces)
|
|
|
|
# convert namespace to route names
|
|
|
|
sub_ns_paths = sub_namespaces.collect { |ns_name, _| "/#{ns_name}" }
|
|
|
|
sub_ns_paths_versioned = sub_namespaces.collect { |ns_name, _| "/:version/#{ns_name}" }
|
|
|
|
# get the actual route definitions for the namespace path names
|
|
|
|
sub_routes = parent_route.collect do |route|
|
|
|
|
route if sub_ns_paths.include?(route.instance_variable_get(:@options)[:namespace]) || sub_ns_paths_versioned.include?(route.instance_variable_get(:@options)[:namespace])
|
|
|
|
end.compact
|
|
|
|
# add all determined routes of the sub namespaces to standalone resource
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_namespace_routes[identifier].push(*sub_routes)
|
2015-02-26 00:42:32 +08:00
|
|
|
else
|
|
|
|
# default case when not explicitly specified or nested == true
|
|
|
|
standalone_namespaces = namespaces.reject { |_, ns| !ns.options.key?(:swagger) || !ns.options[:swagger].key?(:nested) || ns.options[:swagger][:nested] != false }
|
|
|
|
parent_standalone_namespaces = standalone_namespaces.reject { |ns_name, _| !name.start_with?(ns_name) }
|
|
|
|
# add only to the main route if the namespace is not within any other namespace appearing as standalone resource
|
|
|
|
if parent_standalone_namespaces.empty?
|
|
|
|
# default option, append namespace methods to parent route
|
2015-04-29 03:54:23 +08:00
|
|
|
@target_class.combined_namespace_routes[parent_route_name] = [] unless @target_class.combined_namespace_routes.key?(parent_route_name)
|
|
|
|
@target_class.combined_namespace_routes[parent_route_name].push(*namespace_routes)
|
2015-02-26 00:42:32 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def standalone_sub_namespaces(name, namespaces)
|
|
|
|
# assign all nested namespace routes to this resource, too
|
|
|
|
# (unless they are assigned to another standalone namespace themselves)
|
|
|
|
sub_namespaces = {}
|
|
|
|
# fetch all namespaces that are children of the current namespace
|
|
|
|
namespaces.each { |ns_name, ns| sub_namespaces[ns_name] = ns if ns_name.start_with?(name) && ns_name != name }
|
|
|
|
# remove the sub namespaces if they are assigned to another standalone namespace themselves
|
|
|
|
sub_namespaces.each do |sub_name, sub_ns|
|
|
|
|
# skip if sub_ns is standalone, too
|
|
|
|
next unless sub_ns.options.key?(:swagger) && sub_ns.options[:swagger][:nested] == false
|
|
|
|
# remove all namespaces that are nested below this standalone sub_ns
|
|
|
|
sub_namespaces.each { |sub_sub_name, _| sub_namespaces.delete(sub_sub_name) if sub_sub_name.start_with?(sub_name) }
|
|
|
|
end
|
|
|
|
sub_namespaces
|
|
|
|
end
|
|
|
|
|
2015-01-31 01:52:22 +08:00
|
|
|
def get_non_nested_params(params)
|
|
|
|
# Duplicate the params as we are going to modify them
|
|
|
|
dup_params = params.each_with_object(Hash.new) do |(param, value), dparams|
|
|
|
|
dparams[param] = value.dup
|
|
|
|
end
|
|
|
|
|
|
|
|
dup_params.reject do |param, value|
|
|
|
|
is_nested_param = /^#{ Regexp.quote param }\[.+\]$/
|
|
|
|
0 < dup_params.count do |p, _|
|
|
|
|
match = p.match(is_nested_param)
|
|
|
|
dup_params[p][:required] = false if match && !value[:required]
|
|
|
|
match
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-03-03 19:52:26 +08:00
|
|
|
def parse_array_params(params)
|
|
|
|
modified_params = {}
|
|
|
|
array_param = nil
|
|
|
|
params.each_key do |k|
|
|
|
|
if params[k].is_a?(Hash) && params[k][:type] == 'Array'
|
|
|
|
array_param = k
|
2015-03-13 15:22:43 +08:00
|
|
|
modified_params[k] = params[k]
|
2015-03-03 19:52:26 +08:00
|
|
|
else
|
|
|
|
new_key = k
|
|
|
|
unless array_param.nil?
|
|
|
|
if k.to_s.start_with?(array_param.to_s + '[')
|
|
|
|
new_key = array_param.to_s + '[]' + k.to_s.split(array_param)[1]
|
2015-03-13 15:22:43 +08:00
|
|
|
modified_params.delete array_param
|
2015-03-03 19:52:26 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
modified_params[new_key] = params[k]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
modified_params
|
|
|
|
end
|
|
|
|
|
2015-03-17 23:18:59 +08:00
|
|
|
def parse_enum_values(values)
|
|
|
|
if values.is_a?(Range) && [Integer, String].any? { |klass| values.first.is_a?(klass) }
|
|
|
|
values.to_a
|
|
|
|
elsif values.is_a?(Proc)
|
|
|
|
values.call
|
|
|
|
else
|
|
|
|
values
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-07-19 22:15:14 +08:00
|
|
|
def create_documentation_class
|
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
|
2013-10-17 03:43:05 +08:00
|
|
|
|
|
|
|
def as_markdown(description)
|
2014-08-13 17:43:48 +08:00
|
|
|
description && @@markdown ? @@markdown.as_markdown(strip_heredoc(description)) : description
|
2013-10-17 03:43:05 +08:00
|
|
|
end
|
|
|
|
|
2012-07-26 20:41:47 +08:00
|
|
|
def parse_params(params, path, method)
|
2013-12-06 08:48:26 +08:00
|
|
|
params ||= []
|
2014-10-21 19:09:58 +08:00
|
|
|
|
2015-03-03 19:52:26 +08:00
|
|
|
parsed_array_params = parse_array_params(params)
|
|
|
|
|
|
|
|
non_nested_parent_params = get_non_nested_params(parsed_array_params)
|
2014-10-21 19:09:58 +08:00
|
|
|
|
|
|
|
non_nested_parent_params.map do |param, value|
|
2014-05-13 01:54:54 +08:00
|
|
|
items = {}
|
|
|
|
|
2015-04-01 20:10:28 +08:00
|
|
|
raw_data_type = value[:type] if value.is_a?(Hash)
|
|
|
|
raw_data_type ||= 'string'
|
|
|
|
data_type = case raw_data_type.to_s
|
2014-11-29 00:20:37 +08:00
|
|
|
when 'Hash'
|
|
|
|
'object'
|
|
|
|
when 'Rack::Multipart::UploadedFile'
|
|
|
|
'File'
|
2014-11-29 00:05:05 +08:00
|
|
|
when 'Virtus::Attribute::Boolean'
|
|
|
|
'boolean'
|
2015-02-01 11:53:18 +08:00
|
|
|
when 'Boolean', 'Date', 'Integer', 'String', 'Float'
|
2015-04-01 20:10:28 +08:00
|
|
|
raw_data_type.to_s.downcase
|
2014-07-14 21:59:11 +08:00
|
|
|
when 'BigDecimal'
|
|
|
|
'long'
|
|
|
|
when 'DateTime'
|
|
|
|
'dateTime'
|
|
|
|
when 'Numeric'
|
|
|
|
'double'
|
2015-01-26 20:38:37 +08:00
|
|
|
when 'Symbol'
|
|
|
|
'string'
|
2014-05-30 22:41:51 +08:00
|
|
|
else
|
2014-12-01 21:59:20 +08:00
|
|
|
@@documentation_class.parse_entity_name(raw_data_type)
|
2014-05-30 22:41:51 +08:00
|
|
|
end
|
2015-03-12 23:22:13 +08:00
|
|
|
|
|
|
|
additional_documentation = value.is_a?(Hash) ? value[:documentation] : nil
|
|
|
|
|
|
|
|
if additional_documentation && value.is_a?(Hash)
|
|
|
|
value = additional_documentation.merge(value)
|
|
|
|
end
|
|
|
|
|
2014-05-13 01:54:54 +08:00
|
|
|
description = value.is_a?(Hash) ? value[:desc] || value[:description] : ''
|
|
|
|
required = value.is_a?(Hash) ? !!value[:required] : false
|
2014-07-14 22:29:46 +08:00
|
|
|
default_value = value.is_a?(Hash) ? value[:default] : nil
|
2015-03-12 23:22:13 +08:00
|
|
|
example = value.is_a?(Hash) ? value[:example] : nil
|
2014-05-13 01:54:54 +08:00
|
|
|
is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false
|
2015-03-17 23:18:59 +08:00
|
|
|
values = value.is_a?(Hash) ? value[:values] : nil
|
|
|
|
enum_values = parse_enum_values(values)
|
2014-07-30 19:40:37 +08:00
|
|
|
|
2015-03-11 14:24:19 +08:00
|
|
|
if value.is_a?(Hash) && value.key?(:documentation) && value[:documentation].key?(:param_type)
|
|
|
|
param_type = value[:documentation][:param_type]
|
2014-05-13 01:54:54 +08:00
|
|
|
if is_array
|
2014-07-14 21:59:11 +08:00
|
|
|
items = { '$ref' => data_type }
|
|
|
|
data_type = 'array'
|
2014-05-13 01:54:54 +08:00
|
|
|
end
|
2013-08-07 02:43:01 +08:00
|
|
|
else
|
2014-07-14 21:59:11 +08:00
|
|
|
param_type = case
|
2014-05-13 04:15:23 +08:00
|
|
|
when path.include?(":#{param}")
|
|
|
|
'path'
|
2014-07-14 21:59:11 +08:00
|
|
|
when %w(POST PUT PATCH).include?(method)
|
|
|
|
if is_primitive?(data_type)
|
2014-05-13 05:46:00 +08:00
|
|
|
'form'
|
2014-05-31 00:21:23 +08:00
|
|
|
else
|
|
|
|
'body'
|
2014-05-13 05:46:00 +08:00
|
|
|
end
|
2014-05-13 01:54:54 +08:00
|
|
|
else
|
2014-05-13 04:15:23 +08:00
|
|
|
'query'
|
2014-05-13 01:54:54 +08:00
|
|
|
end
|
2013-08-07 02:43:01 +08:00
|
|
|
end
|
2014-05-13 01:54:54 +08:00
|
|
|
name = (value.is_a?(Hash) && value[:full_name]) || param
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2014-02-04 17:26:27 +08:00
|
|
|
parsed_params = {
|
2014-07-14 21:59:11 +08:00
|
|
|
paramType: param_type,
|
2014-05-08 23:25:42 +08:00
|
|
|
name: name,
|
|
|
|
description: as_markdown(description),
|
2014-07-14 21:59:11 +08:00
|
|
|
type: data_type,
|
2014-05-08 23:25:42 +08:00
|
|
|
required: required,
|
|
|
|
allowMultiple: is_array
|
2013-12-06 08:48:26 +08:00
|
|
|
}
|
2015-03-12 23:22:13 +08:00
|
|
|
|
2014-07-14 21:59:11 +08:00
|
|
|
parsed_params.merge!(format: 'int32') if data_type == 'integer'
|
|
|
|
parsed_params.merge!(format: 'int64') if data_type == 'long'
|
|
|
|
parsed_params.merge!(items: items) if items.present?
|
2015-03-12 23:22:13 +08:00
|
|
|
parsed_params.merge!(defaultValue: example) if example
|
|
|
|
if default_value && example.blank?
|
|
|
|
parsed_params.merge!(defaultValue: default_value)
|
|
|
|
end
|
2014-07-30 18:02:46 +08:00
|
|
|
parsed_params.merge!(enum: enum_values) if enum_values
|
2014-02-04 17:26:27 +08:00
|
|
|
parsed_params
|
2012-08-17 03:47:18 +08:00
|
|
|
end
|
|
|
|
end
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2013-12-06 13:10:52 +08:00
|
|
|
def content_types_for(target_class)
|
2014-11-11 04:08:52 +08:00
|
|
|
content_types = (target_class.content_types || {}).values
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2013-12-06 13:10:52 +08:00
|
|
|
if content_types.empty?
|
2014-11-11 04:08:52 +08:00
|
|
|
formats = [target_class.format, target_class.default_format].compact.uniq
|
2013-12-06 13:10:52 +08:00
|
|
|
formats = Grape::Formatter::Base.formatters({}).keys if formats.empty?
|
2014-07-14 21:59:11 +08:00
|
|
|
content_types = Grape::ContentTypes::CONTENT_TYPES.select { |content_type, _mime_type| formats.include? content_type }.values
|
2013-12-06 13:10:52 +08:00
|
|
|
end
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2013-12-06 13:10:52 +08:00
|
|
|
content_types.uniq
|
|
|
|
end
|
2012-08-17 03:47:18 +08:00
|
|
|
|
2013-11-25 22:35:04 +08:00
|
|
|
def parse_info(info)
|
|
|
|
{
|
2013-12-06 09:31:02 +08:00
|
|
|
contact: info[:contact],
|
2014-07-14 23:35:03 +08:00
|
|
|
description: as_markdown(info[:description]),
|
2013-12-06 09:31:02 +08:00
|
|
|
license: info[:license],
|
|
|
|
licenseUrl: info[:license_url],
|
|
|
|
termsOfServiceUrl: info[:terms_of_service_url],
|
|
|
|
title: info[:title]
|
2014-07-14 21:59:11 +08:00
|
|
|
}.delete_if { |_, value| value.blank? }
|
2013-11-25 22:35:04 +08:00
|
|
|
end
|
2012-08-17 03:47:18 +08:00
|
|
|
|
|
|
|
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|
|
2014-07-14 21:59:11 +08:00
|
|
|
data_type = 'String'
|
|
|
|
description = value.is_a?(Hash) ? value[:description] : ''
|
|
|
|
required = value.is_a?(Hash) ? !!value[:required] : false
|
2014-07-14 22:29:46 +08:00
|
|
|
default_value = value.is_a?(Hash) ? value[:default] : nil
|
2014-07-14 21:59:11 +08:00
|
|
|
param_type = 'header'
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2014-02-04 17:26:27 +08:00
|
|
|
parsed_params = {
|
2014-07-14 21:59:11 +08:00
|
|
|
paramType: param_type,
|
2013-12-06 08:48:26 +08:00
|
|
|
name: param,
|
|
|
|
description: as_markdown(description),
|
2014-07-14 21:59:11 +08:00
|
|
|
type: data_type,
|
2014-02-04 17:26:27 +08:00
|
|
|
required: required
|
2013-12-06 08:48:26 +08:00
|
|
|
}
|
2014-02-04 17:26:27 +08:00
|
|
|
|
2014-07-14 21:59:11 +08:00
|
|
|
parsed_params.merge!(defaultValue: default_value) if default_value
|
2014-02-04 17:26:27 +08:00
|
|
|
|
|
|
|
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}')
|
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
|
|
|
|
2014-07-14 23:09:17 +08:00
|
|
|
def parse_entity_name(model)
|
|
|
|
if model.respond_to?(:entity_name)
|
|
|
|
model.entity_name
|
|
|
|
else
|
|
|
|
name = model.to_s
|
|
|
|
entity_parts = name.split('::')
|
|
|
|
entity_parts.reject! { |p| p == 'Entity' || p == 'Entities' }
|
|
|
|
entity_parts.join('::')
|
|
|
|
end
|
2014-02-04 09:15:23 +08:00
|
|
|
end
|
|
|
|
|
2013-07-27 03:12:20 +08:00
|
|
|
def parse_entity_models(models)
|
|
|
|
result = {}
|
2013-12-06 09:50:47 +08:00
|
|
|
models.each do |model|
|
2015-04-27 19:58:11 +08:00
|
|
|
name = (model.instance_variable_get(:@root) || parse_entity_name(model))
|
2014-05-29 03:34:12 +08:00
|
|
|
properties = {}
|
|
|
|
required = []
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2013-12-06 09:50:47 +08:00
|
|
|
model.documentation.each do |property_name, property_info|
|
2014-05-29 03:42:32 +08:00
|
|
|
p = property_info.dup
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2014-07-14 21:59:11 +08:00
|
|
|
required << property_name.to_s if p.delete(:required)
|
2014-05-29 03:34:12 +08:00
|
|
|
|
2015-01-18 02:41:34 +08:00
|
|
|
type = if p[:type]
|
|
|
|
p.delete(:type)
|
2015-03-11 22:42:23 +08:00
|
|
|
else
|
|
|
|
exposure = model.exposures[property_name]
|
|
|
|
parse_entity_name(exposure[:using]) if exposure
|
2015-01-18 02:41:34 +08:00
|
|
|
end
|
|
|
|
|
2014-05-29 03:42:32 +08:00
|
|
|
if p.delete(:is_array)
|
2015-01-18 02:41:34 +08:00
|
|
|
p[:items] = generate_typeref(type)
|
2014-07-14 21:59:11 +08:00
|
|
|
p[:type] = 'array'
|
2014-05-29 04:09:06 +08:00
|
|
|
else
|
2015-01-18 02:41:34 +08:00
|
|
|
p.merge! generate_typeref(type)
|
2014-05-09 00:13:50 +08:00
|
|
|
end
|
|
|
|
|
2013-12-06 09:50:47 +08:00
|
|
|
# rename Grape Entity's "desc" to "description"
|
2014-07-14 21:59:11 +08:00
|
|
|
property_description = p.delete(:desc)
|
|
|
|
p[:description] = property_description if property_description
|
2014-05-29 03:42:32 +08:00
|
|
|
|
2014-07-30 19:40:37 +08:00
|
|
|
# rename Grape's 'values' to 'enum'
|
|
|
|
select_values = p.delete(:values)
|
|
|
|
if select_values
|
|
|
|
select_values = select_values.call if select_values.is_a?(Proc)
|
|
|
|
p[:enum] = select_values
|
|
|
|
end
|
|
|
|
|
2014-05-29 03:42:32 +08:00
|
|
|
properties[property_name] = p
|
2013-12-06 09:40:51 +08:00
|
|
|
end
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2013-07-27 03:12:20 +08:00
|
|
|
result[name] = {
|
2015-04-27 19:58:11 +08:00
|
|
|
id: name,
|
2014-05-31 00:21:23 +08:00
|
|
|
properties: properties
|
2013-07-27 03:12:20 +08:00
|
|
|
}
|
2014-05-31 00:21:23 +08:00
|
|
|
result[name].merge!(required: required) unless required.empty?
|
2013-07-27 03:12:20 +08:00
|
|
|
end
|
2014-02-04 07:54:26 +08:00
|
|
|
|
2013-07-27 03:12:20 +08:00
|
|
|
result
|
|
|
|
end
|
|
|
|
|
2014-07-17 00:20:10 +08:00
|
|
|
def models_with_included_presenters(models)
|
|
|
|
all_models = models
|
|
|
|
|
|
|
|
models.each do |model|
|
2014-08-01 19:31:05 +08:00
|
|
|
# get model references from exposures with a documentation
|
2014-11-22 07:27:25 +08:00
|
|
|
nested_models = model.exposures.map do |_, config|
|
2014-12-16 00:14:43 +08:00
|
|
|
if config.key?(:documentation)
|
|
|
|
model = config[:using]
|
|
|
|
model.respond_to?(:constantize) ? model.constantize : model
|
|
|
|
end
|
2014-08-01 19:31:05 +08:00
|
|
|
end.compact
|
2014-07-17 00:20:10 +08:00
|
|
|
|
2014-11-22 07:27:25 +08:00
|
|
|
# get all nested models recursively
|
|
|
|
additional_models = nested_models.map do |nested_model|
|
|
|
|
models_with_included_presenters([nested_model])
|
|
|
|
end.flatten
|
|
|
|
|
2014-07-17 00:20:10 +08:00
|
|
|
all_models += additional_models
|
|
|
|
end
|
|
|
|
|
|
|
|
all_models
|
|
|
|
end
|
|
|
|
|
2014-05-29 04:09:06 +08:00
|
|
|
def is_primitive?(type)
|
2014-11-29 00:20:37 +08:00
|
|
|
%w(object integer long float double string byte boolean date dateTime).include? type
|
2014-05-29 04:09:06 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def generate_typeref(type)
|
2015-04-01 20:10:28 +08:00
|
|
|
type_s = type.to_s.sub(/^[A-Z]/) { |f| f.downcase }
|
|
|
|
if is_primitive? type_s
|
|
|
|
{ 'type' => type_s }
|
2014-05-29 04:09:06 +08:00
|
|
|
else
|
2015-04-01 20:10:28 +08:00
|
|
|
{ '$ref' => parse_entity_name(type) }
|
2014-05-29 04:09:06 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-07-14 23:04:15 +08:00
|
|
|
def parse_http_codes(codes, models)
|
2013-01-24 12:17:04 +08:00
|
|
|
codes ||= {}
|
2014-07-14 23:04:15 +08:00
|
|
|
codes.map do |k, v, m|
|
|
|
|
models << m if m
|
2014-07-14 23:09:17 +08:00
|
|
|
http_code_hash = {
|
2013-12-06 08:49:24 +08:00
|
|
|
code: k,
|
2014-07-14 23:04:15 +08:00
|
|
|
message: v
|
2013-12-06 08:49:24 +08:00
|
|
|
}
|
2014-07-14 23:09:17 +08:00
|
|
|
http_code_hash[:responseModel] = parse_entity_name(m) if m
|
|
|
|
http_code_hash
|
2013-01-24 12:17:04 +08:00
|
|
|
end
|
|
|
|
end
|
2013-04-04 07:34:09 +08:00
|
|
|
|
|
|
|
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)
|
2013-11-07 17:05:53 +08:00
|
|
|
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
|
2014-12-01 21:59:20 +08:00
|
|
|
|
2014-12-01 23:40:09 +08:00
|
|
|
def hide_documentation_path
|
|
|
|
@@hide_documentation_path
|
|
|
|
end
|
|
|
|
|
|
|
|
def mount_path
|
|
|
|
@@mount_path
|
|
|
|
end
|
|
|
|
|
2014-12-01 21:59:20 +08:00
|
|
|
def setup(options)
|
|
|
|
defaults = {
|
|
|
|
target_class: nil,
|
|
|
|
mount_path: '/swagger_doc',
|
|
|
|
base_path: nil,
|
|
|
|
api_version: '0.1',
|
|
|
|
markdown: nil,
|
|
|
|
hide_documentation_path: false,
|
|
|
|
hide_format: false,
|
|
|
|
format: nil,
|
|
|
|
models: [],
|
|
|
|
info: {},
|
|
|
|
authorizations: nil,
|
|
|
|
root_base_path: true,
|
|
|
|
api_documentation: { desc: 'Swagger compatible API description' },
|
|
|
|
specific_api_documentation: { desc: 'Swagger compatible API description for specific API' }
|
|
|
|
}
|
|
|
|
|
|
|
|
options = defaults.merge(options)
|
|
|
|
|
|
|
|
target_class = options[:target_class]
|
|
|
|
@@mount_path = options[:mount_path]
|
|
|
|
@@class_name = options[:class_name] || options[:mount_path].gsub('/', '')
|
|
|
|
@@markdown = options[:markdown] ? GrapeSwagger::Markdown.new(options[:markdown]) : nil
|
|
|
|
@@hide_format = options[:hide_format]
|
|
|
|
api_version = options[:api_version]
|
|
|
|
authorizations = options[:authorizations]
|
|
|
|
root_base_path = options[:root_base_path]
|
|
|
|
extra_info = options[:info]
|
|
|
|
api_doc = options[:api_documentation].dup
|
|
|
|
specific_api_doc = options[:specific_api_documentation].dup
|
|
|
|
@@models = options[:models] || []
|
|
|
|
|
|
|
|
@@hide_documentation_path = options[:hide_documentation_path]
|
|
|
|
|
|
|
|
if options[:format]
|
|
|
|
[:format, :default_format, :default_error_formatter].each do |method|
|
|
|
|
send(method, options[:format])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@@documentation_class = self
|
|
|
|
|
|
|
|
desc api_doc.delete(:desc), api_doc
|
|
|
|
get @@mount_path do
|
|
|
|
header['Access-Control-Allow-Origin'] = '*'
|
|
|
|
header['Access-Control-Request-Method'] = '*'
|
|
|
|
|
|
|
|
namespaces = target_class.combined_namespaces
|
2015-02-26 00:42:32 +08:00
|
|
|
namespace_routes = target_class.combined_namespace_routes
|
2014-12-01 21:59:20 +08:00
|
|
|
|
|
|
|
if @@hide_documentation_path
|
2015-02-26 00:42:32 +08:00
|
|
|
namespace_routes.reject! { |route, _value| "/#{route}/".index(@@documentation_class.parse_path(@@mount_path, nil) << '/') == 0 }
|
2014-12-01 21:59:20 +08:00
|
|
|
end
|
|
|
|
|
2015-02-26 00:42:32 +08:00
|
|
|
namespace_routes_array = namespace_routes.keys.map do |local_route|
|
|
|
|
next if namespace_routes[local_route].map(&:route_hidden).all? { |value| value.respond_to?(:call) ? value.call : value }
|
2014-12-01 21:59:20 +08:00
|
|
|
|
|
|
|
url_format = '.{format}' unless @@hide_format
|
|
|
|
|
2015-02-26 00:42:32 +08:00
|
|
|
original_namespace_name = target_class.combined_namespace_identifiers.key?(local_route) ? target_class.combined_namespace_identifiers[local_route] : local_route
|
|
|
|
description = namespaces[original_namespace_name] && namespaces[original_namespace_name].options[:desc]
|
|
|
|
description ||= "Operations about #{original_namespace_name.pluralize}"
|
2014-12-01 21:59:20 +08:00
|
|
|
|
|
|
|
{
|
|
|
|
path: "/#{local_route}#{url_format}",
|
|
|
|
description: description
|
|
|
|
}
|
|
|
|
end.compact
|
|
|
|
|
|
|
|
output = {
|
|
|
|
apiVersion: api_version,
|
|
|
|
swaggerVersion: '1.2',
|
|
|
|
produces: @@documentation_class.content_types_for(target_class),
|
2015-02-26 00:42:32 +08:00
|
|
|
apis: namespace_routes_array,
|
2014-12-01 21:59:20 +08:00
|
|
|
info: @@documentation_class.parse_info(extra_info)
|
|
|
|
}
|
|
|
|
|
|
|
|
output[:authorizations] = authorizations unless authorizations.nil? || authorizations.empty?
|
|
|
|
|
|
|
|
output
|
|
|
|
end
|
|
|
|
|
|
|
|
desc specific_api_doc.delete(:desc), { params: {
|
|
|
|
'name' => {
|
|
|
|
desc: 'Resource name of mounted API',
|
|
|
|
type: 'string',
|
|
|
|
required: true
|
|
|
|
}
|
|
|
|
}.merge(specific_api_doc.delete(:params) || {}) }.merge(specific_api_doc)
|
|
|
|
|
|
|
|
get "#{@@mount_path}/:name" do
|
|
|
|
header['Access-Control-Allow-Origin'] = '*'
|
|
|
|
header['Access-Control-Request-Method'] = '*'
|
|
|
|
|
|
|
|
models = []
|
2015-02-26 00:42:32 +08:00
|
|
|
routes = target_class.combined_namespace_routes[params[:name]]
|
2014-12-01 21:59:20 +08:00
|
|
|
error!('Not Found', 404) unless routes
|
|
|
|
|
2015-02-11 23:34:29 +08:00
|
|
|
visible_ops = routes.reject do |route|
|
|
|
|
route.route_hidden.respond_to?(:call) ? route.route_hidden.call : route.route_hidden
|
|
|
|
end
|
|
|
|
|
|
|
|
ops = visible_ops.group_by do |route|
|
2014-12-01 21:59:20 +08:00
|
|
|
@@documentation_class.parse_path(route.route_path, api_version)
|
|
|
|
end
|
|
|
|
|
|
|
|
error!('Not Found', 404) unless ops.any?
|
|
|
|
|
|
|
|
apis = []
|
|
|
|
|
|
|
|
ops.each do |path, op_routes|
|
|
|
|
operations = op_routes.map do |route|
|
|
|
|
notes = @@documentation_class.as_markdown(route.route_notes)
|
|
|
|
|
|
|
|
http_codes = @@documentation_class.parse_http_codes(route.route_http_codes, models)
|
|
|
|
|
|
|
|
models |= @@models if @@models.present?
|
|
|
|
|
2015-02-20 23:12:18 +08:00
|
|
|
models |= Array(route.route_entity) if route.route_entity.present?
|
2014-12-01 21:59:20 +08:00
|
|
|
|
|
|
|
models = @@documentation_class.models_with_included_presenters(models.flatten.compact)
|
|
|
|
|
|
|
|
operation = {
|
|
|
|
notes: notes.to_s,
|
|
|
|
summary: route.route_description || '',
|
|
|
|
nickname: route.route_nickname || (route.route_method + route.route_path.gsub(/[\/:\(\)\.]/, '-')),
|
|
|
|
method: route.route_method,
|
|
|
|
parameters: @@documentation_class.parse_header_params(route.route_headers) + @@documentation_class.parse_params(route.route_params, route.route_path, route.route_method),
|
2015-04-01 20:15:50 +08:00
|
|
|
type: route.route_is_array ? 'array' : 'void'
|
2014-12-01 21:59:20 +08:00
|
|
|
}
|
|
|
|
operation[:authorizations] = route.route_authorizations unless route.route_authorizations.nil? || route.route_authorizations.empty?
|
|
|
|
if operation[:parameters].any? { | param | param[:type] == 'File' }
|
|
|
|
operation.merge!(consumes: ['multipart/form-data'])
|
|
|
|
end
|
|
|
|
operation.merge!(responseMessages: http_codes) unless http_codes.empty?
|
|
|
|
|
|
|
|
if route.route_entity
|
2015-02-20 23:12:18 +08:00
|
|
|
type = @@documentation_class.parse_entity_name(Array(route.route_entity).first)
|
2015-04-01 20:15:50 +08:00
|
|
|
if route.route_is_array
|
|
|
|
operation.merge!(items: { '$ref' => type })
|
|
|
|
else
|
|
|
|
operation.merge!(type: type)
|
|
|
|
end
|
2014-12-01 21:59:20 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
operation[:nickname] = route.route_nickname if route.route_nickname
|
|
|
|
operation
|
|
|
|
end.compact
|
|
|
|
apis << {
|
|
|
|
path: path,
|
|
|
|
operations: operations
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2015-02-26 00:42:32 +08:00
|
|
|
# use custom resource naming if available
|
|
|
|
if target_class.combined_namespace_identifiers.key? params[:name]
|
|
|
|
resource_path = target_class.combined_namespace_identifiers[params[:name]]
|
|
|
|
else
|
|
|
|
resource_path = params[:name]
|
|
|
|
end
|
2014-12-01 21:59:20 +08:00
|
|
|
api_description = {
|
|
|
|
apiVersion: api_version,
|
|
|
|
swaggerVersion: '1.2',
|
2015-02-26 00:42:32 +08:00
|
|
|
resourcePath: "/#{resource_path}",
|
2014-12-01 21:59:20 +08:00
|
|
|
produces: @@documentation_class.content_types_for(target_class),
|
|
|
|
apis: apis
|
|
|
|
}
|
|
|
|
|
|
|
|
base_path = @@documentation_class.parse_base_path(options[:base_path], request)
|
|
|
|
api_description[:basePath] = base_path if base_path && base_path.size > 0 && root_base_path != false
|
|
|
|
api_description[:models] = @@documentation_class.parse_entity_models(models) unless models.empty?
|
|
|
|
api_description[:authorizations] = authorizations if authorizations
|
|
|
|
|
|
|
|
api_description
|
|
|
|
end
|
|
|
|
end
|
2012-07-19 16:37:46 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|