class Grape::API
Attributes
combined_namespaces[R]
combined_routes[R]
Public Class Methods
add_swagger_documentation(options = {})
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 7 def add_swagger_documentation(options = {}) documentation_class = create_documentation_class documentation_class.setup({ target_class: self }.merge(options)) mount(documentation_class) @combined_routes = {} routes.each do |route| route_match = route.route_path.split(route.route_prefix).last.match('\/([\w|-]*?)[\.\/\(]') next if route_match.nil? resource = route_match.captures.first next if resource.empty? resource.downcase! @combined_routes[resource] ||= [] next if @@hide_documentation_path && route.route_path.include?(@@mount_path) @combined_routes[resource] << route end @combined_namespaces = {} combine_namespaces(self) end
Private Class Methods
as_markdown(description)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 223 def as_markdown(description) description && @@markdown ? Kramdown::Document.new(strip_heredoc(description), input: 'GFM', enable_coderay: false).to_html : description end
combine_namespaces(app)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 30 def combine_namespaces(app) app.endpoints.each do |endpoint| ns = if endpoint.respond_to?(:namespace_stackable) endpoint.namespace_stackable(:namespace).last else endpoint.settings.stack.last[:namespace] end # use the full namespace here (not the latest level only) # and strip leading slash @combined_namespaces[endpoint.namespace.sub(/^\//, '')] = ns if ns combine_namespaces(endpoint.options[:app]) if endpoint.options[:app] end end
content_types_for(target_class)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 292 def content_types_for(target_class) content_types = (target_class.settings[:content_types] || {}).values 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 content_types.uniq end
create_documentation_class()
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 45 def create_documentation_class Class.new(Grape::API) do class << self def name @@class_name end end def self.setup(options) defaults = { target_class: nil, mount_path: '/swagger_doc', mount_with_version: false, base_path: nil, api_version: '0.1', markdown: false, 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] api_version = options[:api_version] @@mount_path = options[:mount_path] + ((options[:mount_with_version] and api_version.present?) ? "/#{api_version}" : '') @@class_name = options[:class_name] || options[:mount_path].gsub('/', '') @@markdown = options[:markdown] @@hide_format = options[:hide_format] base_path = options[:base_path] 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 desc api_doc.delete(:desc), params: api_doc.delete(:params) get @@mount_path do header['Access-Control-Allow-Origin'] = '*' header['Access-Control-Request-Method'] = '*' routes = target_class.combined_routes namespaces = target_class.combined_namespaces if @@hide_documentation_path routes.reject! { |route, _value| "/#{route}/".index(parse_path(@@mount_path, nil) << '/') == 0 } end routes_array = routes.keys.map do |local_route| next if routes[local_route].all?(&:route_hidden) url_format = '.{format}' unless @@hide_format description = namespaces[local_route] && namespaces[local_route].options[:desc] description ||= "Operations about #{local_route.pluralize}" { path: "/#{local_route}#{url_format}", description: description } end.compact output = { apiVersion: api_version, swaggerVersion: '1.2', produces: content_types_for(target_class), apis: routes_array, info: 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) || {}) get "#{@@mount_path}/:name" do header['Access-Control-Allow-Origin'] = '*' header['Access-Control-Request-Method'] = '*' models = [] routes = target_class.combined_routes[params[:name]] error!('Not Found', 404) unless routes ops = routes.reject(&:route_hidden).group_by do |route| 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 = as_markdown(route.route_notes) http_codes = parse_http_codes(route.route_http_codes, models) models << @@models if @@models.present? models << route.route_entity if route.route_entity.present? models = 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: parse_header_params(route.route_headers) + parse_params(route.route_params, route.route_path, route.route_method), type: 'void' } 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 type = parse_entity_name(route.route_entity) if route.instance_variable_get(:@options)[:is_array] operation.merge!( 'type' => 'array', 'items' => generate_typeref(type) ) else operation.merge!('type' => type) end end operation[:nickname] = route.route_nickname if route.route_nickname operation end.compact apis << { path: path, operations: operations } end api_description = { apiVersion: api_version, swaggerVersion: '1.2', resourcePath: "/#{params[:name]}", produces: content_types_for(target_class), apis: apis } base_path = parse_base_path(base_path, request) api_description[:basePath] = base_path if base_path && base_path.size > 0 && root_base_path != false api_description[:models] = parse_entity_models(models) unless models.empty? api_description[:authorizations] = authorizations if authorizations api_description end end helpers do def as_markdown(description) description && @@markdown ? Kramdown::Document.new(strip_heredoc(description), input: 'GFM', enable_coderay: false).to_html : description end def parse_params(params, path, method) params ||= [] params.map do |param, value| value[:type] = 'File' if value.is_a?(Hash) && value[:type] == 'Rack::Multipart::UploadedFile' items = {} raw_data_type = value.is_a?(Hash) ? (value[:type] || 'string').to_s : 'string' data_type = case raw_data_type when 'Boolean', 'Date', 'Integer', 'String' raw_data_type.downcase when 'BigDecimal' 'long' when 'DateTime' 'dateTime' when 'Numeric' 'double' else parse_entity_name(raw_data_type) end description = value.is_a?(Hash) ? value[:desc] || value[:description] : '' required = value.is_a?(Hash) ? !!value[:required] : false default_value = value.is_a?(Hash) ? value[:default] : nil is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false enum_values = value.is_a?(Hash) ? value[:values] : nil enum_values = enum_values.call if enum_values && enum_values.is_a?(Proc) if value.is_a?(Hash) && value.key?(:param_type) param_type = value[:param_type] if is_array items = { '$ref' => data_type } data_type = 'array' end else param_type = case when path.include?(":#{param}") 'path' when %w(POST PUT PATCH).include?(method) if is_primitive?(data_type) 'form' else 'body' end else 'query' end end name = (value.is_a?(Hash) && value[:full_name]) || param parsed_params = { paramType: param_type, name: name, description: as_markdown(description), type: data_type, required: required, allowMultiple: is_array } 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? parsed_params.merge!(defaultValue: default_value) if default_value parsed_params.merge!(enum: enum_values) if enum_values parsed_params end end def content_types_for(target_class) content_types = (target_class.settings[:content_types] || {}).values 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 content_types.uniq end def parse_info(info) { contact: info[:contact], description: as_markdown(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) params ||= [] params.map do |param, value| data_type = 'String' description = value.is_a?(Hash) ? value[:description] : '' required = value.is_a?(Hash) ? !!value[:required] : false default_value = value.is_a?(Hash) ? value[:default] : nil param_type = 'header' parsed_params = { paramType: param_type, name: param, description: as_markdown(description), type: data_type, required: required } parsed_params.merge!(defaultValue: default_value) if default_value parsed_params end end def parse_path(path, version) # adapt format to swagger format 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}') # add the version version ? parsed_path.gsub('{version}', version) : parsed_path end 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 end def parse_entity_models(models) result = {} models.each do |model| name = parse_entity_name(model) properties = {} required = [] model.documentation.each do |property_name, property_info| p = property_info.dup required << property_name.to_s if p.delete(:required) if p.delete(:is_array) p[:items] = generate_typeref(p[:type]) p[:type] = 'array' else p.merge! generate_typeref(p.delete(:type)) end # rename Grape Entity's "desc" to "description" property_description = p.delete(:desc) p[:description] = property_description if property_description # 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 properties[property_name] = p end result[name] = { id: model.instance_variable_get(:@root) || name, properties: properties } result[name].merge!(required: required) unless required.empty? end result end def models_with_included_presenters(models) all_models = models models.each do |model| # get model references from exposures with a documentation additional_models = model.exposures.map do |_, config| config[:using] if config.key?(:documentation) end.compact all_models += additional_models end all_models end def is_primitive?(type) %w(integer long float double string byte boolean date dateTime).include? type end def generate_typeref(type) if is_primitive? type { 'type' => type } else { '$ref' => type } end end def parse_http_codes(codes, models) codes ||= {} codes.map do |k, v, m| models << m if m http_code_hash = { code: k, message: v } http_code_hash[:responseModel] = parse_entity_name(m) if m http_code_hash end end def try(*args, &block) if args.empty? && block_given? yield self 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 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 end end end end
generate_typeref(type)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 426 def generate_typeref(type) if is_primitive? type { 'type' => type } else { '$ref' => type } end end
is_primitive?(type)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 422 def is_primitive?(type) %w(integer long float double string byte boolean date dateTime).include? type end
models_with_included_presenters(models)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 407 def models_with_included_presenters(models) all_models = models models.each do |model| # get model references from exposures with a documentation additional_models = model.exposures.map do |_, config| config[:using] if config.key?(:documentation) end.compact all_models += additional_models end all_models end
name()
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 48 def name @@class_name end
parse_base_path(base_path, request)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 460 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 end
parse_entity_models(models)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 363 def parse_entity_models(models) result = {} models.each do |model| name = parse_entity_name(model) properties = {} required = [] model.documentation.each do |property_name, property_info| p = property_info.dup required << property_name.to_s if p.delete(:required) if p.delete(:is_array) p[:items] = generate_typeref(p[:type]) p[:type] = 'array' else p.merge! generate_typeref(p.delete(:type)) end # rename Grape Entity's "desc" to "description" property_description = p.delete(:desc) p[:description] = property_description if property_description # 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 properties[property_name] = p end result[name] = { id: model.instance_variable_get(:@root) || name, properties: properties } result[name].merge!(required: required) unless required.empty? end result end
parse_entity_name(model)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 352 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 end
parse_header_params(params)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 315 def parse_header_params(params) params ||= [] params.map do |param, value| data_type = 'String' description = value.is_a?(Hash) ? value[:description] : '' required = value.is_a?(Hash) ? !!value[:required] : false default_value = value.is_a?(Hash) ? value[:default] : nil param_type = 'header' parsed_params = { paramType: param_type, name: param, description: as_markdown(description), type: data_type, required: required } parsed_params.merge!(defaultValue: default_value) if default_value parsed_params end end
parse_http_codes(codes, models)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 434 def parse_http_codes(codes, models) codes ||= {} codes.map do |k, v, m| models << m if m http_code_hash = { code: k, message: v } http_code_hash[:responseModel] = parse_entity_name(m) if m http_code_hash end end
parse_info(info)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 304 def parse_info(info) { contact: info[:contact], description: as_markdown(info[:description]), license: info[:license], licenseUrl: info[:license_url], termsOfServiceUrl: info[:terms_of_service_url], title: info[:title] }.delete_if { |_, value| value.blank? } end
parse_params(params, path, method)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 227 def parse_params(params, path, method) params ||= [] params.map do |param, value| value[:type] = 'File' if value.is_a?(Hash) && value[:type] == 'Rack::Multipart::UploadedFile' items = {} raw_data_type = value.is_a?(Hash) ? (value[:type] || 'string').to_s : 'string' data_type = case raw_data_type when 'Boolean', 'Date', 'Integer', 'String' raw_data_type.downcase when 'BigDecimal' 'long' when 'DateTime' 'dateTime' when 'Numeric' 'double' else parse_entity_name(raw_data_type) end description = value.is_a?(Hash) ? value[:desc] || value[:description] : '' required = value.is_a?(Hash) ? !!value[:required] : false default_value = value.is_a?(Hash) ? value[:default] : nil is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false enum_values = value.is_a?(Hash) ? value[:values] : nil enum_values = enum_values.call if enum_values && enum_values.is_a?(Proc) if value.is_a?(Hash) && value.key?(:param_type) param_type = value[:param_type] if is_array items = { '$ref' => data_type } data_type = 'array' end else param_type = case when path.include?(":#{param}") 'path' when %w(POST PUT PATCH).include?(method) if is_primitive?(data_type) 'form' else 'body' end else 'query' end end name = (value.is_a?(Hash) && value[:full_name]) || param parsed_params = { paramType: param_type, name: name, description: as_markdown(description), type: data_type, required: required, allowMultiple: is_array } 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? parsed_params.merge!(defaultValue: default_value) if default_value parsed_params.merge!(enum: enum_values) if enum_values parsed_params end end
parse_path(path, version)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 339 def parse_path(path, version) # adapt format to swagger format 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}') # add the version version ? parsed_path.gsub('{version}', version) : parsed_path end
setup(options)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 53 def self.setup(options) defaults = { target_class: nil, mount_path: '/swagger_doc', mount_with_version: false, base_path: nil, api_version: '0.1', markdown: false, 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] api_version = options[:api_version] @@mount_path = options[:mount_path] + ((options[:mount_with_version] and api_version.present?) ? "/#{api_version}" : '') @@class_name = options[:class_name] || options[:mount_path].gsub('/', '') @@markdown = options[:markdown] @@hide_format = options[:hide_format] base_path = options[:base_path] 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 desc api_doc.delete(:desc), params: api_doc.delete(:params) get @@mount_path do header['Access-Control-Allow-Origin'] = '*' header['Access-Control-Request-Method'] = '*' routes = target_class.combined_routes namespaces = target_class.combined_namespaces if @@hide_documentation_path routes.reject! { |route, _value| "/#{route}/".index(parse_path(@@mount_path, nil) << '/') == 0 } end routes_array = routes.keys.map do |local_route| next if routes[local_route].all?(&:route_hidden) url_format = '.{format}' unless @@hide_format description = namespaces[local_route] && namespaces[local_route].options[:desc] description ||= "Operations about #{local_route.pluralize}" { path: "/#{local_route}#{url_format}", description: description } end.compact output = { apiVersion: api_version, swaggerVersion: '1.2', produces: content_types_for(target_class), apis: routes_array, info: 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) || {}) get "#{@@mount_path}/:name" do header['Access-Control-Allow-Origin'] = '*' header['Access-Control-Request-Method'] = '*' models = [] routes = target_class.combined_routes[params[:name]] error!('Not Found', 404) unless routes ops = routes.reject(&:route_hidden).group_by do |route| 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 = as_markdown(route.route_notes) http_codes = parse_http_codes(route.route_http_codes, models) models << @@models if @@models.present? models << route.route_entity if route.route_entity.present? models = 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: parse_header_params(route.route_headers) + parse_params(route.route_params, route.route_path, route.route_method), type: 'void' } 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 type = parse_entity_name(route.route_entity) if route.instance_variable_get(:@options)[:is_array] operation.merge!( 'type' => 'array', 'items' => generate_typeref(type) ) else operation.merge!('type' => type) end end operation[:nickname] = route.route_nickname if route.route_nickname operation end.compact apis << { path: path, operations: operations } end api_description = { apiVersion: api_version, swaggerVersion: '1.2', resourcePath: "/#{params[:name]}", produces: content_types_for(target_class), apis: apis } base_path = parse_base_path(base_path, request) api_description[:basePath] = base_path if base_path && base_path.size > 0 && root_base_path != false api_description[:models] = parse_entity_models(models) unless models.empty? api_description[:authorizations] = authorizations if authorizations api_description end end
strip_heredoc(string)
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 455 def strip_heredoc(string) indent = string.scan(/^[ \t]*(?=\S)/).min.try(:size) || 0 string.gsub(/^[ \t]{#{indent}}/, '') end
try(*args) { |self| ... }
click to toggle source
# File lib/swagger/grape_swagger_modified.rb, line 447 def try(*args, &block) if args.empty? && block_given? yield self elsif respond_to?(args.first) public_send(*args, &block) end end