gitlab-grape-swagger 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (166) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.github/dependabot.yml +20 -0
  4. data/.github/workflows/ci.yml +45 -0
  5. data/.gitignore +44 -0
  6. data/.gitlab-ci.yml +19 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +136 -0
  9. data/.rubocop_todo.yml +60 -0
  10. data/.ruby-gemset +1 -0
  11. data/CHANGELOG.md +671 -0
  12. data/CONTRIBUTING.md +126 -0
  13. data/Dangerfile +3 -0
  14. data/Gemfile +45 -0
  15. data/Gemfile.lock +249 -0
  16. data/LICENSE.txt +20 -0
  17. data/README.md +1772 -0
  18. data/RELEASING.md +82 -0
  19. data/Rakefile +20 -0
  20. data/UPGRADING.md +201 -0
  21. data/example/api/endpoints.rb +131 -0
  22. data/example/api/entities.rb +18 -0
  23. data/example/config.ru +42 -0
  24. data/example/example_requests.postman_collection +146 -0
  25. data/example/splines.png +0 -0
  26. data/example/swagger-example.png +0 -0
  27. data/grape-swagger.gemspec +23 -0
  28. data/lib/grape-swagger/doc_methods/build_model_definition.rb +68 -0
  29. data/lib/grape-swagger/doc_methods/data_type.rb +110 -0
  30. data/lib/grape-swagger/doc_methods/extensions.rb +101 -0
  31. data/lib/grape-swagger/doc_methods/file_params.rb +17 -0
  32. data/lib/grape-swagger/doc_methods/format_data.rb +53 -0
  33. data/lib/grape-swagger/doc_methods/headers.rb +20 -0
  34. data/lib/grape-swagger/doc_methods/move_params.rb +209 -0
  35. data/lib/grape-swagger/doc_methods/operation_id.rb +32 -0
  36. data/lib/grape-swagger/doc_methods/optional_object.rb +30 -0
  37. data/lib/grape-swagger/doc_methods/parse_params.rb +190 -0
  38. data/lib/grape-swagger/doc_methods/path_string.rb +52 -0
  39. data/lib/grape-swagger/doc_methods/produces_consumes.rb +15 -0
  40. data/lib/grape-swagger/doc_methods/status_codes.rb +21 -0
  41. data/lib/grape-swagger/doc_methods/tag_name_description.rb +34 -0
  42. data/lib/grape-swagger/doc_methods/version.rb +20 -0
  43. data/lib/grape-swagger/doc_methods.rb +142 -0
  44. data/lib/grape-swagger/endpoint/params_parser.rb +76 -0
  45. data/lib/grape-swagger/endpoint.rb +476 -0
  46. data/lib/grape-swagger/errors.rb +17 -0
  47. data/lib/grape-swagger/instance.rb +7 -0
  48. data/lib/grape-swagger/model_parsers.rb +42 -0
  49. data/lib/grape-swagger/rake/oapi_tasks.rb +135 -0
  50. data/lib/grape-swagger/version.rb +5 -0
  51. data/lib/grape-swagger.rb +174 -0
  52. data/spec/issues/267_nested_namespaces.rb +55 -0
  53. data/spec/issues/403_versions_spec.rb +124 -0
  54. data/spec/issues/427_entity_as_string_spec.rb +39 -0
  55. data/spec/issues/430_entity_definitions_spec.rb +94 -0
  56. data/spec/issues/532_allow_custom_format_spec.rb +42 -0
  57. data/spec/issues/533_specify_status_code_spec.rb +78 -0
  58. data/spec/issues/537_enum_values_spec.rb +50 -0
  59. data/spec/issues/539_array_post_body_spec.rb +65 -0
  60. data/spec/issues/542_array_of_type_in_post_body_spec.rb +46 -0
  61. data/spec/issues/553_align_array_put_post_params_spec.rb +152 -0
  62. data/spec/issues/572_array_post_body_spec.rb +51 -0
  63. data/spec/issues/579_align_put_post_parameters_spec.rb +185 -0
  64. data/spec/issues/582_file_response_spec.rb +55 -0
  65. data/spec/issues/587_range_parameter_delimited_by_dash_spec.rb +26 -0
  66. data/spec/issues/605_root_route_documentation_spec.rb +23 -0
  67. data/spec/issues/650_params_array_spec.rb +65 -0
  68. data/spec/issues/677_consumes_produces_add_swagger_documentation_options_spec.rb +100 -0
  69. data/spec/issues/680_keep_204_error_schemas_spec.rb +55 -0
  70. data/spec/issues/721_set_default_parameter_location_based_on_consumes_spec.rb +62 -0
  71. data/spec/issues/751_deeply_nested_objects_spec.rb +190 -0
  72. data/spec/issues/776_multiple_presents_spec.rb +59 -0
  73. data/spec/issues/784_extensions_on_params_spec.rb +42 -0
  74. data/spec/issues/809_utf8_routes_spec.rb +55 -0
  75. data/spec/issues/832_array_hash_float_decimal_spec.rb +114 -0
  76. data/spec/issues/847_route_param_options_spec.rb +37 -0
  77. data/spec/issues/873_wildcard_segments_path_parameters_spec.rb +28 -0
  78. data/spec/issues/878_optional_path_segments_spec.rb +29 -0
  79. data/spec/issues/881_handle_file_params_spec.rb +38 -0
  80. data/spec/issues/883_query_array_parameter_spec.rb +46 -0
  81. data/spec/issues/884_dont_document_non_schema_examples_spec.rb +49 -0
  82. data/spec/issues/887_prevent_duplicate_operation_ids_spec.rb +35 -0
  83. data/spec/lib/data_type_spec.rb +111 -0
  84. data/spec/lib/endpoint/params_parser_spec.rb +124 -0
  85. data/spec/lib/endpoint_spec.rb +153 -0
  86. data/spec/lib/extensions_spec.rb +185 -0
  87. data/spec/lib/format_data_spec.rb +115 -0
  88. data/spec/lib/model_parsers_spec.rb +104 -0
  89. data/spec/lib/move_params_spec.rb +444 -0
  90. data/spec/lib/oapi_tasks_spec.rb +163 -0
  91. data/spec/lib/operation_id_spec.rb +55 -0
  92. data/spec/lib/optional_object_spec.rb +47 -0
  93. data/spec/lib/parse_params_spec.rb +68 -0
  94. data/spec/lib/path_string_spec.rb +101 -0
  95. data/spec/lib/produces_consumes_spec.rb +116 -0
  96. data/spec/lib/tag_name_description_spec.rb +80 -0
  97. data/spec/lib/version_spec.rb +28 -0
  98. data/spec/spec_helper.rb +39 -0
  99. data/spec/support/empty_model_parser.rb +23 -0
  100. data/spec/support/grape_version.rb +13 -0
  101. data/spec/support/mock_parser.rb +23 -0
  102. data/spec/support/model_parsers/entity_parser.rb +334 -0
  103. data/spec/support/model_parsers/mock_parser.rb +346 -0
  104. data/spec/support/model_parsers/representable_parser.rb +406 -0
  105. data/spec/support/namespace_tags.rb +93 -0
  106. data/spec/support/the_paths_definitions.rb +109 -0
  107. data/spec/swagger_v2/api_documentation_spec.rb +42 -0
  108. data/spec/swagger_v2/api_swagger_v2_additional_properties_spec.rb +83 -0
  109. data/spec/swagger_v2/api_swagger_v2_body_definitions_spec.rb +48 -0
  110. data/spec/swagger_v2/api_swagger_v2_definitions-models_spec.rb +36 -0
  111. data/spec/swagger_v2/api_swagger_v2_detail_spec.rb +79 -0
  112. data/spec/swagger_v2/api_swagger_v2_extensions_spec.rb +145 -0
  113. data/spec/swagger_v2/api_swagger_v2_format-content_type_spec.rb +137 -0
  114. data/spec/swagger_v2/api_swagger_v2_global_configuration_spec.rb +56 -0
  115. data/spec/swagger_v2/api_swagger_v2_hash_and_array_spec.rb +64 -0
  116. data/spec/swagger_v2/api_swagger_v2_headers_spec.rb +58 -0
  117. data/spec/swagger_v2/api_swagger_v2_hide_documentation_path_spec.rb +57 -0
  118. data/spec/swagger_v2/api_swagger_v2_hide_param_spec.rb +109 -0
  119. data/spec/swagger_v2/api_swagger_v2_ignore_defaults_spec.rb +48 -0
  120. data/spec/swagger_v2/api_swagger_v2_mounted_spec.rb +153 -0
  121. data/spec/swagger_v2/api_swagger_v2_param_type_body_nested_spec.rb +355 -0
  122. data/spec/swagger_v2/api_swagger_v2_param_type_body_spec.rb +217 -0
  123. data/spec/swagger_v2/api_swagger_v2_param_type_spec.rb +247 -0
  124. data/spec/swagger_v2/api_swagger_v2_request_params_fix_spec.rb +80 -0
  125. data/spec/swagger_v2/api_swagger_v2_response_spec.rb +147 -0
  126. data/spec/swagger_v2/api_swagger_v2_response_with_examples_spec.rb +135 -0
  127. data/spec/swagger_v2/api_swagger_v2_response_with_headers_spec.rb +216 -0
  128. data/spec/swagger_v2/api_swagger_v2_response_with_models_spec.rb +53 -0
  129. data/spec/swagger_v2/api_swagger_v2_response_with_root_spec.rb +153 -0
  130. data/spec/swagger_v2/api_swagger_v2_spec.rb +245 -0
  131. data/spec/swagger_v2/api_swagger_v2_status_codes_spec.rb +93 -0
  132. data/spec/swagger_v2/api_swagger_v2_type-format_spec.rb +90 -0
  133. data/spec/swagger_v2/boolean_params_spec.rb +38 -0
  134. data/spec/swagger_v2/default_api_spec.rb +175 -0
  135. data/spec/swagger_v2/deprecated_field_spec.rb +25 -0
  136. data/spec/swagger_v2/description_not_initialized_spec.rb +39 -0
  137. data/spec/swagger_v2/endpoint_versioned_path_spec.rb +130 -0
  138. data/spec/swagger_v2/errors_spec.rb +77 -0
  139. data/spec/swagger_v2/float_api_spec.rb +36 -0
  140. data/spec/swagger_v2/form_params_spec.rb +76 -0
  141. data/spec/swagger_v2/grape-swagger_spec.rb +17 -0
  142. data/spec/swagger_v2/guarded_endpoint_spec.rb +162 -0
  143. data/spec/swagger_v2/hide_api_spec.rb +147 -0
  144. data/spec/swagger_v2/host_spec.rb +43 -0
  145. data/spec/swagger_v2/inheritance_and_discriminator_spec.rb +57 -0
  146. data/spec/swagger_v2/mount_override_api_spec.rb +58 -0
  147. data/spec/swagger_v2/mounted_target_class_spec.rb +76 -0
  148. data/spec/swagger_v2/namespace_tags_prefix_spec.rb +122 -0
  149. data/spec/swagger_v2/namespace_tags_spec.rb +78 -0
  150. data/spec/swagger_v2/namespaced_api_spec.rb +121 -0
  151. data/spec/swagger_v2/nicknamed_api_spec.rb +25 -0
  152. data/spec/swagger_v2/operation_id_api_spec.rb +27 -0
  153. data/spec/swagger_v2/param_multi_type_spec.rb +82 -0
  154. data/spec/swagger_v2/param_type_spec.rb +95 -0
  155. data/spec/swagger_v2/param_values_spec.rb +180 -0
  156. data/spec/swagger_v2/params_array_collection_format_spec.rb +105 -0
  157. data/spec/swagger_v2/params_array_spec.rb +225 -0
  158. data/spec/swagger_v2/params_example_spec.rb +38 -0
  159. data/spec/swagger_v2/params_hash_spec.rb +77 -0
  160. data/spec/swagger_v2/params_nested_spec.rb +92 -0
  161. data/spec/swagger_v2/parent_less_namespace_spec.rb +32 -0
  162. data/spec/swagger_v2/reference_entity_spec.rb +129 -0
  163. data/spec/swagger_v2/security_requirement_spec.rb +46 -0
  164. data/spec/swagger_v2/simple_mounted_api_spec.rb +332 -0
  165. data/spec/version_spec.rb +10 -0
  166. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ GrapeInstance = if defined? Grape::API::Instance
4
+ Grape::API::Instance
5
+ else
6
+ Grape::API
7
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrapeSwagger
4
+ VERSION = '1.5.0'
5
+ end