gitlab-grape-swagger 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.github/dependabot.yml +20 -0
- data/.github/workflows/ci.yml +45 -0
- data/.gitignore +44 -0
- data/.gitlab-ci.yml +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +136 -0
- data/.rubocop_todo.yml +60 -0
- data/.ruby-gemset +1 -0
- data/CHANGELOG.md +671 -0
- data/CONTRIBUTING.md +126 -0
- data/Dangerfile +3 -0
- data/Gemfile +45 -0
- data/Gemfile.lock +249 -0
- data/LICENSE.txt +20 -0
- data/README.md +1772 -0
- data/RELEASING.md +82 -0
- data/Rakefile +20 -0
- data/UPGRADING.md +201 -0
- data/example/api/endpoints.rb +131 -0
- data/example/api/entities.rb +18 -0
- data/example/config.ru +42 -0
- data/example/example_requests.postman_collection +146 -0
- data/example/splines.png +0 -0
- data/example/swagger-example.png +0 -0
- data/grape-swagger.gemspec +23 -0
- data/lib/grape-swagger/doc_methods/build_model_definition.rb +68 -0
- data/lib/grape-swagger/doc_methods/data_type.rb +110 -0
- data/lib/grape-swagger/doc_methods/extensions.rb +101 -0
- data/lib/grape-swagger/doc_methods/file_params.rb +17 -0
- data/lib/grape-swagger/doc_methods/format_data.rb +53 -0
- data/lib/grape-swagger/doc_methods/headers.rb +20 -0
- data/lib/grape-swagger/doc_methods/move_params.rb +209 -0
- data/lib/grape-swagger/doc_methods/operation_id.rb +32 -0
- data/lib/grape-swagger/doc_methods/optional_object.rb +30 -0
- data/lib/grape-swagger/doc_methods/parse_params.rb +190 -0
- data/lib/grape-swagger/doc_methods/path_string.rb +52 -0
- data/lib/grape-swagger/doc_methods/produces_consumes.rb +15 -0
- data/lib/grape-swagger/doc_methods/status_codes.rb +21 -0
- data/lib/grape-swagger/doc_methods/tag_name_description.rb +34 -0
- data/lib/grape-swagger/doc_methods/version.rb +20 -0
- data/lib/grape-swagger/doc_methods.rb +142 -0
- data/lib/grape-swagger/endpoint/params_parser.rb +76 -0
- data/lib/grape-swagger/endpoint.rb +476 -0
- data/lib/grape-swagger/errors.rb +17 -0
- data/lib/grape-swagger/instance.rb +7 -0
- data/lib/grape-swagger/model_parsers.rb +42 -0
- data/lib/grape-swagger/rake/oapi_tasks.rb +135 -0
- data/lib/grape-swagger/version.rb +5 -0
- data/lib/grape-swagger.rb +174 -0
- data/spec/issues/267_nested_namespaces.rb +55 -0
- data/spec/issues/403_versions_spec.rb +124 -0
- data/spec/issues/427_entity_as_string_spec.rb +39 -0
- data/spec/issues/430_entity_definitions_spec.rb +94 -0
- data/spec/issues/532_allow_custom_format_spec.rb +42 -0
- data/spec/issues/533_specify_status_code_spec.rb +78 -0
- data/spec/issues/537_enum_values_spec.rb +50 -0
- data/spec/issues/539_array_post_body_spec.rb +65 -0
- data/spec/issues/542_array_of_type_in_post_body_spec.rb +46 -0
- data/spec/issues/553_align_array_put_post_params_spec.rb +152 -0
- data/spec/issues/572_array_post_body_spec.rb +51 -0
- data/spec/issues/579_align_put_post_parameters_spec.rb +185 -0
- data/spec/issues/582_file_response_spec.rb +55 -0
- data/spec/issues/587_range_parameter_delimited_by_dash_spec.rb +26 -0
- data/spec/issues/605_root_route_documentation_spec.rb +23 -0
- data/spec/issues/650_params_array_spec.rb +65 -0
- data/spec/issues/677_consumes_produces_add_swagger_documentation_options_spec.rb +100 -0
- data/spec/issues/680_keep_204_error_schemas_spec.rb +55 -0
- data/spec/issues/721_set_default_parameter_location_based_on_consumes_spec.rb +62 -0
- data/spec/issues/751_deeply_nested_objects_spec.rb +190 -0
- data/spec/issues/776_multiple_presents_spec.rb +59 -0
- data/spec/issues/784_extensions_on_params_spec.rb +42 -0
- data/spec/issues/809_utf8_routes_spec.rb +55 -0
- data/spec/issues/832_array_hash_float_decimal_spec.rb +114 -0
- data/spec/issues/847_route_param_options_spec.rb +37 -0
- data/spec/issues/873_wildcard_segments_path_parameters_spec.rb +28 -0
- data/spec/issues/878_optional_path_segments_spec.rb +29 -0
- data/spec/issues/881_handle_file_params_spec.rb +38 -0
- data/spec/issues/883_query_array_parameter_spec.rb +46 -0
- data/spec/issues/884_dont_document_non_schema_examples_spec.rb +49 -0
- data/spec/issues/887_prevent_duplicate_operation_ids_spec.rb +35 -0
- data/spec/lib/data_type_spec.rb +111 -0
- data/spec/lib/endpoint/params_parser_spec.rb +124 -0
- data/spec/lib/endpoint_spec.rb +153 -0
- data/spec/lib/extensions_spec.rb +185 -0
- data/spec/lib/format_data_spec.rb +115 -0
- data/spec/lib/model_parsers_spec.rb +104 -0
- data/spec/lib/move_params_spec.rb +444 -0
- data/spec/lib/oapi_tasks_spec.rb +163 -0
- data/spec/lib/operation_id_spec.rb +55 -0
- data/spec/lib/optional_object_spec.rb +47 -0
- data/spec/lib/parse_params_spec.rb +68 -0
- data/spec/lib/path_string_spec.rb +101 -0
- data/spec/lib/produces_consumes_spec.rb +116 -0
- data/spec/lib/tag_name_description_spec.rb +80 -0
- data/spec/lib/version_spec.rb +28 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/empty_model_parser.rb +23 -0
- data/spec/support/grape_version.rb +13 -0
- data/spec/support/mock_parser.rb +23 -0
- data/spec/support/model_parsers/entity_parser.rb +334 -0
- data/spec/support/model_parsers/mock_parser.rb +346 -0
- data/spec/support/model_parsers/representable_parser.rb +406 -0
- data/spec/support/namespace_tags.rb +93 -0
- data/spec/support/the_paths_definitions.rb +109 -0
- data/spec/swagger_v2/api_documentation_spec.rb +42 -0
- data/spec/swagger_v2/api_swagger_v2_additional_properties_spec.rb +83 -0
- data/spec/swagger_v2/api_swagger_v2_body_definitions_spec.rb +48 -0
- data/spec/swagger_v2/api_swagger_v2_definitions-models_spec.rb +36 -0
- data/spec/swagger_v2/api_swagger_v2_detail_spec.rb +79 -0
- data/spec/swagger_v2/api_swagger_v2_extensions_spec.rb +145 -0
- data/spec/swagger_v2/api_swagger_v2_format-content_type_spec.rb +137 -0
- data/spec/swagger_v2/api_swagger_v2_global_configuration_spec.rb +56 -0
- data/spec/swagger_v2/api_swagger_v2_hash_and_array_spec.rb +64 -0
- data/spec/swagger_v2/api_swagger_v2_headers_spec.rb +58 -0
- data/spec/swagger_v2/api_swagger_v2_hide_documentation_path_spec.rb +57 -0
- data/spec/swagger_v2/api_swagger_v2_hide_param_spec.rb +109 -0
- data/spec/swagger_v2/api_swagger_v2_ignore_defaults_spec.rb +48 -0
- data/spec/swagger_v2/api_swagger_v2_mounted_spec.rb +153 -0
- data/spec/swagger_v2/api_swagger_v2_param_type_body_nested_spec.rb +355 -0
- data/spec/swagger_v2/api_swagger_v2_param_type_body_spec.rb +217 -0
- data/spec/swagger_v2/api_swagger_v2_param_type_spec.rb +247 -0
- data/spec/swagger_v2/api_swagger_v2_request_params_fix_spec.rb +80 -0
- data/spec/swagger_v2/api_swagger_v2_response_spec.rb +147 -0
- data/spec/swagger_v2/api_swagger_v2_response_with_examples_spec.rb +135 -0
- data/spec/swagger_v2/api_swagger_v2_response_with_headers_spec.rb +216 -0
- data/spec/swagger_v2/api_swagger_v2_response_with_models_spec.rb +53 -0
- data/spec/swagger_v2/api_swagger_v2_response_with_root_spec.rb +153 -0
- data/spec/swagger_v2/api_swagger_v2_spec.rb +245 -0
- data/spec/swagger_v2/api_swagger_v2_status_codes_spec.rb +93 -0
- data/spec/swagger_v2/api_swagger_v2_type-format_spec.rb +90 -0
- data/spec/swagger_v2/boolean_params_spec.rb +38 -0
- data/spec/swagger_v2/default_api_spec.rb +175 -0
- data/spec/swagger_v2/deprecated_field_spec.rb +25 -0
- data/spec/swagger_v2/description_not_initialized_spec.rb +39 -0
- data/spec/swagger_v2/endpoint_versioned_path_spec.rb +130 -0
- data/spec/swagger_v2/errors_spec.rb +77 -0
- data/spec/swagger_v2/float_api_spec.rb +36 -0
- data/spec/swagger_v2/form_params_spec.rb +76 -0
- data/spec/swagger_v2/grape-swagger_spec.rb +17 -0
- data/spec/swagger_v2/guarded_endpoint_spec.rb +162 -0
- data/spec/swagger_v2/hide_api_spec.rb +147 -0
- data/spec/swagger_v2/host_spec.rb +43 -0
- data/spec/swagger_v2/inheritance_and_discriminator_spec.rb +57 -0
- data/spec/swagger_v2/mount_override_api_spec.rb +58 -0
- data/spec/swagger_v2/mounted_target_class_spec.rb +76 -0
- data/spec/swagger_v2/namespace_tags_prefix_spec.rb +122 -0
- data/spec/swagger_v2/namespace_tags_spec.rb +78 -0
- data/spec/swagger_v2/namespaced_api_spec.rb +121 -0
- data/spec/swagger_v2/nicknamed_api_spec.rb +25 -0
- data/spec/swagger_v2/operation_id_api_spec.rb +27 -0
- data/spec/swagger_v2/param_multi_type_spec.rb +82 -0
- data/spec/swagger_v2/param_type_spec.rb +95 -0
- data/spec/swagger_v2/param_values_spec.rb +180 -0
- data/spec/swagger_v2/params_array_collection_format_spec.rb +105 -0
- data/spec/swagger_v2/params_array_spec.rb +225 -0
- data/spec/swagger_v2/params_example_spec.rb +38 -0
- data/spec/swagger_v2/params_hash_spec.rb +77 -0
- data/spec/swagger_v2/params_nested_spec.rb +92 -0
- data/spec/swagger_v2/parent_less_namespace_spec.rb +32 -0
- data/spec/swagger_v2/reference_entity_spec.rb +129 -0
- data/spec/swagger_v2/security_requirement_spec.rb +46 -0
- data/spec/swagger_v2/simple_mounted_api_spec.rb +332 -0
- data/spec/version_spec.rb +10 -0
- metadata +225 -0
@@ -0,0 +1,476 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
5
|
+
require 'grape-swagger/endpoint/params_parser'
|
6
|
+
|
7
|
+
module Grape
|
8
|
+
class Endpoint
|
9
|
+
def content_types_for(target_class)
|
10
|
+
content_types = (target_class.content_types || {}).values
|
11
|
+
|
12
|
+
if content_types.empty?
|
13
|
+
formats = [target_class.format, target_class.default_format].compact.uniq
|
14
|
+
formats = Grape::Formatter.formatters(**{}).keys if formats.empty?
|
15
|
+
content_types = Grape::ContentTypes::CONTENT_TYPES.select do |content_type, _mime_type|
|
16
|
+
formats.include? content_type
|
17
|
+
end.values
|
18
|
+
end
|
19
|
+
|
20
|
+
content_types.uniq
|
21
|
+
end
|
22
|
+
|
23
|
+
# swagger spec2.0 related parts
|
24
|
+
#
|
25
|
+
# required keys for SwaggerObject
|
26
|
+
def swagger_object(target_class, request, options)
|
27
|
+
object = {
|
28
|
+
info: info_object(options[:info].merge(version: options[:doc_version])),
|
29
|
+
swagger: '2.0',
|
30
|
+
produces: options[:produces] || content_types_for(target_class),
|
31
|
+
consumes: options[:consumes],
|
32
|
+
authorizations: options[:authorizations],
|
33
|
+
securityDefinitions: options[:security_definitions],
|
34
|
+
security: options[:security],
|
35
|
+
host: GrapeSwagger::DocMethods::OptionalObject.build(:host, options, request),
|
36
|
+
basePath: GrapeSwagger::DocMethods::OptionalObject.build(:base_path, options, request),
|
37
|
+
schemes: options[:schemes].is_a?(String) ? [options[:schemes]] : options[:schemes]
|
38
|
+
}
|
39
|
+
|
40
|
+
GrapeSwagger::DocMethods::Extensions.add_extensions_to_root(options, object)
|
41
|
+
object.delete_if { |_, value| value.blank? }
|
42
|
+
end
|
43
|
+
|
44
|
+
# building info object
|
45
|
+
def info_object(infos)
|
46
|
+
result = {
|
47
|
+
title: infos[:title] || 'API title',
|
48
|
+
description: infos[:description],
|
49
|
+
termsOfService: infos[:terms_of_service_url],
|
50
|
+
contact: contact_object(infos),
|
51
|
+
license: license_object(infos),
|
52
|
+
version: infos[:version]
|
53
|
+
}
|
54
|
+
|
55
|
+
GrapeSwagger::DocMethods::Extensions.add_extensions_to_info(infos, result)
|
56
|
+
|
57
|
+
result.delete_if { |_, value| value.blank? }
|
58
|
+
end
|
59
|
+
|
60
|
+
# sub-objects of info object
|
61
|
+
# license
|
62
|
+
def license_object(infos)
|
63
|
+
{
|
64
|
+
name: infos.delete(:license),
|
65
|
+
url: infos.delete(:license_url)
|
66
|
+
}.delete_if { |_, value| value.blank? }
|
67
|
+
end
|
68
|
+
|
69
|
+
# contact
|
70
|
+
def contact_object(infos)
|
71
|
+
{
|
72
|
+
name: infos.delete(:contact_name),
|
73
|
+
email: infos.delete(:contact_email),
|
74
|
+
url: infos.delete(:contact_url)
|
75
|
+
}.delete_if { |_, value| value.blank? }
|
76
|
+
end
|
77
|
+
|
78
|
+
# building path and definitions objects
|
79
|
+
def path_and_definition_objects(namespace_routes, options)
|
80
|
+
@paths = {}
|
81
|
+
@definitions = {}
|
82
|
+
add_definitions_from options[:models]
|
83
|
+
namespace_routes.each_value do |routes|
|
84
|
+
path_item(routes, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
[@paths, @definitions]
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_definitions_from(models)
|
91
|
+
return if models.nil?
|
92
|
+
|
93
|
+
models.each { |x| expose_params_from_model(x) }
|
94
|
+
end
|
95
|
+
|
96
|
+
# path object
|
97
|
+
def path_item(routes, options)
|
98
|
+
operation_ids = {}
|
99
|
+
routes.each do |route|
|
100
|
+
next if hidden?(route, options)
|
101
|
+
|
102
|
+
GrapeSwagger::DocMethods::PathString.generate_optional_segments(route.path.dup).each do |path|
|
103
|
+
@item, path = GrapeSwagger::DocMethods::PathString.build(route, path, options)
|
104
|
+
@entity = route.entity || route.options[:success]
|
105
|
+
verb, method_object = method_object(route, options, path)
|
106
|
+
if operation_ids.include?(method_object[:operationId])
|
107
|
+
operation_ids[method_object[:operationId]] += 1
|
108
|
+
method_object[:operationId] += operation_ids[method_object[:operationId]].to_s
|
109
|
+
else
|
110
|
+
operation_ids[method_object[:operationId]] = 1
|
111
|
+
end
|
112
|
+
if @paths.key?(path.to_s)
|
113
|
+
@paths[path.to_s][verb] = method_object
|
114
|
+
else
|
115
|
+
@paths[path.to_s] = { verb => method_object }
|
116
|
+
end
|
117
|
+
GrapeSwagger::DocMethods::Extensions.add(@paths[path.to_s], @definitions, route)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def method_object(route, options, path)
|
123
|
+
method = {}
|
124
|
+
method[:summary] = summary_object(route)
|
125
|
+
method[:description] = description_object(route)
|
126
|
+
method[:produces] = produces_object(route, options[:produces] || options[:format])
|
127
|
+
method[:consumes] = consumes_object(route, options[:consumes] || options[:format])
|
128
|
+
method[:parameters] = params_object(route, options, path, method[:consumes])
|
129
|
+
# if any parameters are file type, automatically set consumes
|
130
|
+
if method[:parameters].present? &&
|
131
|
+
GrapeSwagger::DocMethods::FileParams.includes_file_param?(method[:parameters]) &&
|
132
|
+
['application/x-www-form-urlencoded', 'multipart/form-data'].none? do |consume|
|
133
|
+
method[:consumes].include?(consume)
|
134
|
+
end
|
135
|
+
method[:consumes] = ['application/x-www-form-urlencoded', 'multipart/form-data']
|
136
|
+
end
|
137
|
+
method[:security] = security_object(route)
|
138
|
+
method[:responses] = response_object(route, options)
|
139
|
+
method[:tags] = route.options.fetch(:tags, tag_object(route, path))
|
140
|
+
method[:operationId] = GrapeSwagger::DocMethods::OperationId.build(route, path)
|
141
|
+
method[:deprecated] = deprecated_object(route)
|
142
|
+
method.delete_if { |_, value| value.nil? }
|
143
|
+
|
144
|
+
[route.request_method.downcase.to_sym, method]
|
145
|
+
end
|
146
|
+
|
147
|
+
def deprecated_object(route)
|
148
|
+
route.options[:deprecated] if route.options.key?(:deprecated)
|
149
|
+
end
|
150
|
+
|
151
|
+
def security_object(route)
|
152
|
+
route.options[:security] if route.options.key?(:security)
|
153
|
+
end
|
154
|
+
|
155
|
+
def summary_object(route)
|
156
|
+
summary = route.options[:desc] if route.options.key?(:desc)
|
157
|
+
summary = route.description if route.description.present? && route.options.key?(:detail)
|
158
|
+
summary = route.options[:summary] if route.options.key?(:summary)
|
159
|
+
|
160
|
+
summary
|
161
|
+
end
|
162
|
+
|
163
|
+
def description_object(route)
|
164
|
+
description = route.description if route.description.present?
|
165
|
+
description = route.options[:detail] if route.options.key?(:detail)
|
166
|
+
|
167
|
+
description
|
168
|
+
end
|
169
|
+
|
170
|
+
def produces_object(route, format)
|
171
|
+
return ['application/octet-stream'] if file_response?(route.attributes.success) &&
|
172
|
+
!route.attributes.produces.present?
|
173
|
+
|
174
|
+
mime_types = GrapeSwagger::DocMethods::ProducesConsumes.call(format)
|
175
|
+
|
176
|
+
route_mime_types = %i[formats content_types produces].map do |producer|
|
177
|
+
possible = route.options[producer]
|
178
|
+
GrapeSwagger::DocMethods::ProducesConsumes.call(possible) if possible.present?
|
179
|
+
end.flatten.compact.uniq
|
180
|
+
|
181
|
+
route_mime_types.present? ? route_mime_types : mime_types
|
182
|
+
end
|
183
|
+
|
184
|
+
SUPPORTS_CONSUMES = %i[post put patch].freeze
|
185
|
+
|
186
|
+
def consumes_object(route, format)
|
187
|
+
return unless SUPPORTS_CONSUMES.include?(route.request_method.downcase.to_sym)
|
188
|
+
|
189
|
+
GrapeSwagger::DocMethods::ProducesConsumes.call(route.settings.dig(:description, :consumes) || format)
|
190
|
+
end
|
191
|
+
|
192
|
+
def params_object(route, options, path, consumes)
|
193
|
+
parameters = build_request_params(route, options).each_with_object([]) do |(param, value), memo|
|
194
|
+
next if hidden_parameter?(value)
|
195
|
+
|
196
|
+
value = { required: false }.merge(value) if value.is_a?(Hash)
|
197
|
+
_, value = default_type([[param, value]]).first if value == ''
|
198
|
+
|
199
|
+
if value.dig(:documentation, :type)
|
200
|
+
expose_params(value[:documentation][:type])
|
201
|
+
elsif value[:type]
|
202
|
+
expose_params(value[:type])
|
203
|
+
end
|
204
|
+
memo << GrapeSwagger::DocMethods::ParseParams.call(param, value, path, route, @definitions, consumes)
|
205
|
+
end
|
206
|
+
|
207
|
+
if GrapeSwagger::DocMethods::FileParams.includes_file_param?(parameters)
|
208
|
+
parameters = GrapeSwagger::DocMethods::FileParams.to_formdata(parameters)
|
209
|
+
elsif GrapeSwagger::DocMethods::MoveParams.can_be_moved?(route.request_method, parameters)
|
210
|
+
parameters = GrapeSwagger::DocMethods::MoveParams.to_definition(path, parameters, route, @definitions)
|
211
|
+
end
|
212
|
+
|
213
|
+
GrapeSwagger::DocMethods::FormatData.to_format(parameters)
|
214
|
+
|
215
|
+
parameters.presence
|
216
|
+
end
|
217
|
+
|
218
|
+
def response_object(route, options)
|
219
|
+
codes(route).each_with_object({}) do |value, memo|
|
220
|
+
value[:message] ||= ''
|
221
|
+
memo[value[:code]] = { description: value[:message] ||= '' } unless memo[value[:code]].present?
|
222
|
+
memo[value[:code]][:headers] = value[:headers] if value[:headers]
|
223
|
+
|
224
|
+
next build_file_response(memo[value[:code]]) if file_response?(value[:model])
|
225
|
+
|
226
|
+
if memo.key?(200) && route.request_method == 'DELETE' && value[:model].nil?
|
227
|
+
memo[204] = memo.delete(200)
|
228
|
+
value[:code] = 204
|
229
|
+
next
|
230
|
+
end
|
231
|
+
|
232
|
+
# Explicitly request no model with { model: '' }
|
233
|
+
next if value[:model] == ''
|
234
|
+
|
235
|
+
response_model = value[:model] ? expose_params_from_model(value[:model]) : @item
|
236
|
+
next unless @definitions[response_model]
|
237
|
+
next if response_model.start_with?('Swagger_doc')
|
238
|
+
|
239
|
+
@definitions[response_model][:description] ||= "#{response_model} model"
|
240
|
+
build_memo_schema(memo, route, value, response_model, options)
|
241
|
+
memo[value[:code]][:examples] = value[:examples] if value[:examples]
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def codes(route)
|
246
|
+
http_codes_from_route(route).map do |x|
|
247
|
+
x.is_a?(Array) ? { code: x[0], message: x[1], model: x[2], examples: x[3], headers: x[4] } : x
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def success_code?(code)
|
252
|
+
status = code.is_a?(Array) ? code.first : code[:code]
|
253
|
+
status.between?(200, 299)
|
254
|
+
end
|
255
|
+
|
256
|
+
def http_codes_from_route(route)
|
257
|
+
if route.http_codes.is_a?(Array) && route.http_codes.any? { |code| success_code?(code) }
|
258
|
+
route.http_codes.clone
|
259
|
+
else
|
260
|
+
success_codes_from_route(route) + (route.http_codes || route.options[:failure] || [])
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def success_codes_from_route(route)
|
265
|
+
if @entity.is_a?(Array)
|
266
|
+
return @entity.map do |entity|
|
267
|
+
success_code_from_entity(route, entity)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
[success_code_from_entity(route, @entity)]
|
272
|
+
end
|
273
|
+
|
274
|
+
def tag_object(route, path)
|
275
|
+
version = GrapeSwagger::DocMethods::Version.get(route)
|
276
|
+
version = Array(version)
|
277
|
+
prefix = route.prefix.to_s.split('/').reject(&:empty?)
|
278
|
+
Array(
|
279
|
+
path.split('{')[0].split('/').reject(&:empty?).delete_if do |i|
|
280
|
+
prefix.include?(i) || version.map(&:to_s).include?(i)
|
281
|
+
end.first
|
282
|
+
).presence
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def build_memo_schema(memo, route, value, response_model, options)
|
288
|
+
if memo[value[:code]][:schema] && value[:as]
|
289
|
+
memo[value[:code]][:schema][:properties].merge!(build_reference(route, value, response_model, options))
|
290
|
+
|
291
|
+
if value[:required]
|
292
|
+
memo[value[:code]][:schema][:required] ||= []
|
293
|
+
memo[value[:code]][:schema][:required] << value[:as].to_s
|
294
|
+
end
|
295
|
+
|
296
|
+
elsif value[:as]
|
297
|
+
memo[value[:code]][:schema] = {
|
298
|
+
type: :object,
|
299
|
+
properties: build_reference(route, value, response_model, options)
|
300
|
+
}
|
301
|
+
memo[value[:code]][:schema][:required] = [value[:as].to_s] if value[:required]
|
302
|
+
else
|
303
|
+
memo[value[:code]][:schema] = build_reference(route, value, response_model, options)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def build_reference(route, value, response_model, settings)
|
308
|
+
# TODO: proof that the definition exist, if model isn't specified
|
309
|
+
reference = if value.key?(:as)
|
310
|
+
{ value[:as] => build_reference_hash(response_model) }
|
311
|
+
else
|
312
|
+
build_reference_hash(response_model)
|
313
|
+
end
|
314
|
+
return reference unless value[:code] < 300
|
315
|
+
|
316
|
+
if value.key?(:as) && value.key?(:is_array)
|
317
|
+
reference[value[:as]] = build_reference_array(reference[value[:as]])
|
318
|
+
elsif route.options[:is_array]
|
319
|
+
reference = build_reference_array(reference)
|
320
|
+
end
|
321
|
+
|
322
|
+
build_root(route, reference, response_model, settings)
|
323
|
+
end
|
324
|
+
|
325
|
+
def build_reference_hash(response_model)
|
326
|
+
{ '$ref' => "#/definitions/#{response_model}" }
|
327
|
+
end
|
328
|
+
|
329
|
+
def build_reference_array(reference)
|
330
|
+
{ type: 'array', items: reference }
|
331
|
+
end
|
332
|
+
|
333
|
+
def build_root(route, reference, response_model, settings)
|
334
|
+
default_root = response_model.underscore
|
335
|
+
default_root = default_root.pluralize if route.options[:is_array]
|
336
|
+
case route.settings.dig(:swagger, :root)
|
337
|
+
when true
|
338
|
+
{ type: 'object', properties: { default_root => reference } }
|
339
|
+
when false
|
340
|
+
reference
|
341
|
+
when nil
|
342
|
+
settings[:add_root] ? { type: 'object', properties: { default_root => reference } } : reference
|
343
|
+
else
|
344
|
+
{ type: 'object', properties: { route.settings.dig(:swagger, :root) => reference } }
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def file_response?(value)
|
349
|
+
value.to_s.casecmp('file').zero?
|
350
|
+
end
|
351
|
+
|
352
|
+
def build_file_response(memo)
|
353
|
+
memo['schema'] = { type: 'file' }
|
354
|
+
end
|
355
|
+
|
356
|
+
def build_request_params(route, settings)
|
357
|
+
required = merge_params(route)
|
358
|
+
required = GrapeSwagger::DocMethods::Headers.parse(route) + required unless route.headers.nil?
|
359
|
+
|
360
|
+
default_type(required)
|
361
|
+
|
362
|
+
request_params = GrapeSwagger::Endpoint::ParamsParser.parse_request_params(required, settings, self)
|
363
|
+
|
364
|
+
request_params.empty? ? required : request_params
|
365
|
+
end
|
366
|
+
|
367
|
+
def merge_params(route)
|
368
|
+
path_params = get_path_params(route.app&.inheritable_setting&.namespace_stackable)
|
369
|
+
param_keys = route.params.keys
|
370
|
+
|
371
|
+
# Merge path params options into route params
|
372
|
+
route_params = route.params
|
373
|
+
route_params.each_key do |key|
|
374
|
+
path = path_params[key] || {}
|
375
|
+
params = route_params[key]
|
376
|
+
params = {} unless params.is_a? Hash
|
377
|
+
route_params[key] = path.merge(params)
|
378
|
+
end
|
379
|
+
|
380
|
+
route.params.delete_if { |key| key.is_a?(String) && param_keys.include?(key.to_sym) }.to_a
|
381
|
+
end
|
382
|
+
|
383
|
+
# Iterates over namespaces recursively
|
384
|
+
# to build a hash of path params with options, including type
|
385
|
+
def get_path_params(stackable_values)
|
386
|
+
params = {}
|
387
|
+
return param unless stackable_values
|
388
|
+
return params unless stackable_values.is_a? Grape::Util::StackableValues
|
389
|
+
|
390
|
+
stackable_values&.new_values&.dig(:namespace)&.each do |namespace|
|
391
|
+
space = namespace.space.to_s.gsub(':', '')
|
392
|
+
params[space] = namespace.options || {}
|
393
|
+
end
|
394
|
+
inherited_params = get_path_params(stackable_values.inherited_values)
|
395
|
+
inherited_params.merge(params)
|
396
|
+
end
|
397
|
+
|
398
|
+
def default_type(params)
|
399
|
+
default_param_type = { required: true, type: 'Integer' }
|
400
|
+
params.each { |param| param[-1] = param.last.empty? ? default_param_type : param.last }
|
401
|
+
end
|
402
|
+
|
403
|
+
def expose_params(value)
|
404
|
+
if value.is_a?(Class) && GrapeSwagger.model_parsers.find(value)
|
405
|
+
expose_params_from_model(value)
|
406
|
+
elsif value.is_a?(String)
|
407
|
+
begin
|
408
|
+
expose_params(Object.const_get(value.gsub(/\[|\]/, ''))) # try to load class from its name
|
409
|
+
rescue NameError
|
410
|
+
nil
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def expose_params_from_model(model)
|
416
|
+
model = model.is_a?(String) ? model.constantize : model
|
417
|
+
model_name = model_name(model)
|
418
|
+
|
419
|
+
return model_name if @definitions.key?(model_name)
|
420
|
+
|
421
|
+
@definitions[model_name] = nil
|
422
|
+
|
423
|
+
parser = GrapeSwagger.model_parsers.find(model)
|
424
|
+
raise GrapeSwagger::Errors::UnregisteredParser, "No parser registered for #{model_name}." unless parser
|
425
|
+
|
426
|
+
parsed_response = parser.new(model, self).call
|
427
|
+
|
428
|
+
@definitions[model_name] =
|
429
|
+
GrapeSwagger::DocMethods::BuildModelDefinition.parse_params_from_model(parsed_response, model, model_name)
|
430
|
+
|
431
|
+
model_name
|
432
|
+
end
|
433
|
+
|
434
|
+
def model_name(name)
|
435
|
+
GrapeSwagger::DocMethods::DataType.parse_entity_name(name)
|
436
|
+
end
|
437
|
+
|
438
|
+
def hidden?(route, options)
|
439
|
+
route_hidden = route.settings.try(:[], :swagger).try(:[], :hidden)
|
440
|
+
route_hidden = route.options[:hidden] if route.options.key?(:hidden)
|
441
|
+
return route_hidden unless route_hidden.is_a?(Proc)
|
442
|
+
|
443
|
+
options[:token_owner] ? route_hidden.call(send(options[:token_owner].to_sym)) : route_hidden.call
|
444
|
+
end
|
445
|
+
|
446
|
+
def hidden_parameter?(value)
|
447
|
+
return false if value[:required]
|
448
|
+
|
449
|
+
if value.dig(:documentation, :hidden).is_a?(Proc)
|
450
|
+
value.dig(:documentation, :hidden).call
|
451
|
+
else
|
452
|
+
value.dig(:documentation, :hidden)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def success_code_from_entity(route, entity)
|
457
|
+
default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym]
|
458
|
+
if entity.is_a?(Hash)
|
459
|
+
default_code[:code] = entity[:code] if entity[:code].present?
|
460
|
+
default_code[:model] = entity[:model] if entity[:model].present?
|
461
|
+
default_code[:message] = entity[:message] || route.description || default_code[:message].sub('{item}', @item)
|
462
|
+
default_code[:examples] = entity[:examples] if entity[:examples]
|
463
|
+
default_code[:headers] = entity[:headers] if entity[:headers]
|
464
|
+
default_code[:as] = entity[:as] if entity[:as]
|
465
|
+
default_code[:is_array] = entity[:is_array] if entity[:is_array]
|
466
|
+
default_code[:required] = entity[:required] if entity[:required]
|
467
|
+
else
|
468
|
+
default_code = GrapeSwagger::DocMethods::StatusCodes.get[route.request_method.downcase.to_sym]
|
469
|
+
default_code[:model] = entity if entity
|
470
|
+
default_code[:message] = route.description || default_code[:message].sub('{item}', @item)
|
471
|
+
end
|
472
|
+
|
473
|
+
default_code
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrapeSwagger
|
4
|
+
module Errors
|
5
|
+
class UnregisteredParser < StandardError; end
|
6
|
+
|
7
|
+
class SwaggerSpec < StandardError; end
|
8
|
+
|
9
|
+
class SwaggerSpecDeprecated < SwaggerSpec
|
10
|
+
class << self
|
11
|
+
def tell!(what)
|
12
|
+
warn "[GrapeSwagger] usage of #{what} is deprecated"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrapeSwagger
|
4
|
+
class ModelParsers
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@parsers = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def register(klass, ancestor)
|
12
|
+
@parsers[klass] = ancestor.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def insert_before(before_klass, klass, ancestor)
|
16
|
+
subhash = @parsers.except(klass).to_a
|
17
|
+
insert_at = subhash.index(subhash.assoc(before_klass))
|
18
|
+
insert_at = subhash.length - 1 if insert_at.nil?
|
19
|
+
@parsers = subhash.insert(insert_at, [klass, ancestor]).to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
def insert_after(after_klass, klass, ancestor)
|
23
|
+
subhash = @parsers.except(klass).to_a
|
24
|
+
insert_at = subhash.index(subhash.assoc(after_klass))
|
25
|
+
insert_at = subhash.length - 1 if insert_at.nil?
|
26
|
+
@parsers = subhash.insert(insert_at + 1, [klass, ancestor]).to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
def each
|
30
|
+
@parsers.each_pair do |klass, ancestor|
|
31
|
+
yield klass, ancestor
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def find(model)
|
36
|
+
GrapeSwagger.model_parsers.each do |klass, ancestor|
|
37
|
+
return klass if model.ancestors.map(&:to_s).include?(ancestor)
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/tasklib'
|
5
|
+
require 'rack/test'
|
6
|
+
|
7
|
+
module GrapeSwagger
|
8
|
+
module Rake
|
9
|
+
class OapiTasks < ::Rake::TaskLib
|
10
|
+
include Rack::Test::Methods
|
11
|
+
|
12
|
+
attr_reader :oapi
|
13
|
+
|
14
|
+
def initialize(api_class)
|
15
|
+
super()
|
16
|
+
|
17
|
+
if api_class.is_a? String
|
18
|
+
@api_class_name = api_class
|
19
|
+
else
|
20
|
+
@api_class = api_class
|
21
|
+
end
|
22
|
+
|
23
|
+
define_tasks
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def api_class
|
29
|
+
@api_class ||= @api_class_name.constantize
|
30
|
+
end
|
31
|
+
|
32
|
+
def define_tasks
|
33
|
+
namespace :oapi do
|
34
|
+
fetch
|
35
|
+
validate
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# tasks
|
40
|
+
#
|
41
|
+
# get swagger/OpenAPI documentation
|
42
|
+
def fetch
|
43
|
+
desc 'generates OpenApi documentation …
|
44
|
+
params (usage: key=value):
|
45
|
+
store – save as JSON file, default: false (optional)
|
46
|
+
resource - if given only for that it would be generated (optional)'
|
47
|
+
task fetch: :environment do
|
48
|
+
# :nocov:
|
49
|
+
urls_for(api_class).each do |url|
|
50
|
+
make_request(url)
|
51
|
+
|
52
|
+
save_to_file? ? File.write(file(url), @oapi) : $stdout.print(@oapi)
|
53
|
+
end
|
54
|
+
|
55
|
+
# :nocov:
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# validates swagger/OpenAPI documentation
|
60
|
+
def validate
|
61
|
+
desc 'validates the generated OpenApi file …
|
62
|
+
params (usage: key=value):
|
63
|
+
resource - if given only for that it would be generated (optional)'
|
64
|
+
task validate: :environment do
|
65
|
+
# :nocov:
|
66
|
+
ENV['store'] = 'true'
|
67
|
+
::Rake::Task['oapi:fetch'].invoke
|
68
|
+
exit if error?
|
69
|
+
|
70
|
+
urls_for(api_class).each do |url|
|
71
|
+
@output = system "swagger-cli validate #{file(url)}"
|
72
|
+
|
73
|
+
FileUtils.rm(
|
74
|
+
file(url)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
$stdout.puts 'install swagger-cli with `npm install swagger-cli -g`' if @output.nil?
|
79
|
+
# :nocov:
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# helper methods
|
84
|
+
#
|
85
|
+
# rubocop:disable Style/StringConcatenation
|
86
|
+
def make_request(url)
|
87
|
+
get url
|
88
|
+
|
89
|
+
@oapi = JSON.pretty_generate(
|
90
|
+
JSON.parse(last_response.body, symolize_names: true)
|
91
|
+
) + "\n"
|
92
|
+
end
|
93
|
+
# rubocop:enable Style/StringConcatenation
|
94
|
+
|
95
|
+
def urls_for(api_class)
|
96
|
+
api_class.routes
|
97
|
+
.map(&:path)
|
98
|
+
.select { |e| e.include?('swagger_doc') }
|
99
|
+
.reject { |e| e.include?(':name') }
|
100
|
+
.map { |e| format_path(e) }
|
101
|
+
.map { |e| [e, ENV.fetch('resource', nil)].join('/').chomp('/') }
|
102
|
+
end
|
103
|
+
|
104
|
+
def format_path(path)
|
105
|
+
oapi_route = api_class.routes.select { |e| e.path == path }.first
|
106
|
+
path = path.sub(/\(\.\w+\)$/, '').sub(/\(\.:\w+\)$/, '')
|
107
|
+
path.sub(':version', oapi_route.version.to_s)
|
108
|
+
end
|
109
|
+
|
110
|
+
def save_to_file?
|
111
|
+
ENV['store'].present? && !error?
|
112
|
+
end
|
113
|
+
|
114
|
+
def error?
|
115
|
+
JSON.parse(@oapi).keys.first == 'error'
|
116
|
+
end
|
117
|
+
|
118
|
+
def file(url)
|
119
|
+
api_version = url.split('/').last
|
120
|
+
|
121
|
+
name = if ENV['store'] == 'true' || ENV['store'].blank?
|
122
|
+
"swagger_doc_#{api_version}.json"
|
123
|
+
else
|
124
|
+
ENV['store'].sub('.json', "_#{api_version}.json")
|
125
|
+
end
|
126
|
+
|
127
|
+
File.join(Dir.getwd, name)
|
128
|
+
end
|
129
|
+
|
130
|
+
def app
|
131
|
+
api_class.new
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|