gitlab-grape-swagger 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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