grape 1.8.0 → 4.0.1

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 (317) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +517 -1
  3. data/CONTRIBUTING.md +19 -11
  4. data/README.md +841 -545
  5. data/UPGRADING.md +1553 -7
  6. data/grape.gemspec +11 -14
  7. data/lib/grape/api/instance.rb +77 -160
  8. data/lib/grape/api.rb +75 -107
  9. data/lib/grape/content_types.rb +67 -9
  10. data/lib/grape/cookies.rb +28 -25
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -6
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +25 -63
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/headers.rb +2 -2
  18. data/lib/grape/dsl/helpers.rb +83 -64
  19. data/lib/grape/dsl/inside_route.rb +69 -296
  20. data/lib/grape/dsl/logger.rb +4 -9
  21. data/lib/grape/dsl/middleware.rb +22 -40
  22. data/lib/grape/dsl/parameters.rb +88 -94
  23. data/lib/grape/dsl/request_response.rb +143 -147
  24. data/lib/grape/dsl/rescue_options.rb +25 -0
  25. data/lib/grape/dsl/routing.rb +333 -191
  26. data/lib/grape/dsl/settings.rb +29 -135
  27. data/lib/grape/dsl/validations.rb +39 -32
  28. data/lib/grape/dsl/version_options.rb +23 -0
  29. data/lib/grape/endpoint/options.rb +25 -0
  30. data/lib/grape/endpoint.rb +319 -269
  31. data/lib/grape/{util/env.rb → env.rb} +8 -8
  32. data/lib/grape/error_formatter/base.rb +57 -21
  33. data/lib/grape/error_formatter/json.rb +13 -15
  34. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  35. data/lib/grape/error_formatter/txt.rb +12 -18
  36. data/lib/grape/error_formatter/xml.rb +3 -13
  37. data/lib/grape/error_formatter.rb +9 -25
  38. data/lib/grape/exceptions/base.rb +22 -58
  39. data/lib/grape/exceptions/error_response.rb +48 -0
  40. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  41. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  42. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  44. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  45. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  46. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  47. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  48. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  49. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  50. data/lib/grape/exceptions/request_error.rb +11 -0
  51. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  52. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  53. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  54. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  55. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  56. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  57. data/lib/grape/exceptions/validation.rb +28 -11
  58. data/lib/grape/exceptions/validation_array_errors.rb +5 -0
  59. data/lib/grape/exceptions/validation_errors.rb +22 -26
  60. data/lib/grape/formatter/base.rb +16 -0
  61. data/lib/grape/formatter/json.rb +5 -6
  62. data/lib/grape/formatter/serializable_hash.rb +7 -10
  63. data/lib/grape/formatter/txt.rb +3 -5
  64. data/lib/grape/formatter/xml.rb +4 -6
  65. data/lib/grape/formatter.rb +7 -25
  66. data/lib/grape/json.rb +46 -0
  67. data/lib/grape/locale/en.yml +44 -42
  68. data/lib/grape/middleware/auth/base.rb +11 -33
  69. data/lib/grape/middleware/auth/dsl.rb +12 -34
  70. data/lib/grape/middleware/auth/strategies.rb +1 -2
  71. data/lib/grape/middleware/base.rb +56 -33
  72. data/lib/grape/middleware/error.rb +290 -91
  73. data/lib/grape/middleware/formatter.rb +164 -106
  74. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  75. data/lib/grape/middleware/stack.rb +38 -40
  76. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  77. data/lib/grape/middleware/versioner/base.rb +66 -0
  78. data/lib/grape/middleware/versioner/header.rb +44 -129
  79. data/lib/grape/middleware/versioner/param.rb +4 -25
  80. data/lib/grape/middleware/versioner/path.rb +48 -25
  81. data/lib/grape/middleware/versioner.rb +7 -14
  82. data/lib/grape/mountable.rb +22 -0
  83. data/lib/grape/namespace.rb +21 -14
  84. data/lib/grape/params_builder/base.rb +20 -0
  85. data/lib/grape/params_builder/hash.rb +11 -0
  86. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  87. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  88. data/lib/grape/params_builder.rb +15 -0
  89. data/lib/grape/parser/base.rb +16 -0
  90. data/lib/grape/parser/json.rb +6 -8
  91. data/lib/grape/parser/xml.rb +6 -8
  92. data/lib/grape/parser.rb +5 -23
  93. data/lib/grape/path.rb +8 -94
  94. data/lib/grape/precompiled_json.rb +50 -0
  95. data/lib/grape/railtie.rb +9 -0
  96. data/lib/grape/request.rb +200 -26
  97. data/lib/grape/router/base_route.rb +85 -0
  98. data/lib/grape/router/greedy_route.rb +30 -0
  99. data/lib/grape/router/mustermann_pattern.rb +44 -0
  100. data/lib/grape/router/pattern/path.rb +78 -0
  101. data/lib/grape/router/pattern.rb +77 -35
  102. data/lib/grape/router/route.rb +79 -60
  103. data/lib/grape/router.rb +128 -104
  104. data/lib/grape/serve_stream/file_body.rb +7 -0
  105. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  106. data/lib/grape/serve_stream/stream_response.rb +7 -0
  107. data/lib/grape/testing.rb +33 -0
  108. data/lib/grape/util/api_description.rb +67 -0
  109. data/lib/grape/util/cache.rb +22 -5
  110. data/lib/grape/util/deep_freeze.rb +34 -0
  111. data/lib/grape/util/endpoint_configuration.rb +1 -1
  112. data/lib/grape/util/freeze_on_new.rb +20 -0
  113. data/lib/grape/util/header.rb +13 -0
  114. data/lib/grape/util/inheritable_setting.rb +777 -39
  115. data/lib/grape/util/lazy/base.rb +16 -0
  116. data/lib/grape/util/lazy/block.rb +22 -0
  117. data/lib/grape/util/lazy/value.rb +31 -0
  118. data/lib/grape/util/lazy/value_array.rb +21 -0
  119. data/lib/grape/util/lazy/value_enumerable.rb +31 -0
  120. data/lib/grape/util/lazy/value_hash.rb +21 -0
  121. data/lib/grape/util/media_type.rb +74 -0
  122. data/lib/grape/util/path_normalizer.rb +37 -0
  123. data/lib/grape/util/registry.rb +37 -0
  124. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  125. data/lib/grape/util/stackable_values.rb +40 -16
  126. data/lib/grape/util/translation.rb +42 -0
  127. data/lib/grape/validations/attributes_iterator.rb +61 -28
  128. data/lib/grape/validations/coerce_options.rb +21 -0
  129. data/lib/grape/validations/contract_scope.rb +29 -0
  130. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  131. data/lib/grape/validations/oneof_collector.rb +35 -0
  132. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  133. data/lib/grape/validations/params_documentation.rb +52 -0
  134. data/lib/grape/validations/params_scope.rb +223 -306
  135. data/lib/grape/validations/shared_options.rb +19 -0
  136. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  137. data/lib/grape/validations/types/array_coercer.rb +7 -12
  138. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  139. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  140. data/lib/grape/validations/types/dry_type_coercer.rb +17 -28
  141. data/lib/grape/validations/types/json.rb +1 -5
  142. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  143. data/lib/grape/validations/types/primitive_coercer.rb +14 -35
  144. data/lib/grape/validations/types/set_coercer.rb +1 -4
  145. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  146. data/lib/grape/validations/types.rb +29 -54
  147. data/lib/grape/validations/validations_spec.rb +164 -0
  148. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  149. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  150. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  151. data/lib/grape/validations/validators/base.rb +118 -37
  152. data/lib/grape/validations/validators/coerce_validator.rb +26 -38
  153. data/lib/grape/validations/validators/contract_scope_validator.rb +46 -0
  154. data/lib/grape/validations/validators/default_validator.rb +13 -16
  155. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  156. data/lib/grape/validations/validators/except_values_validator.rb +15 -5
  157. data/lib/grape/validations/validators/length_validator.rb +50 -0
  158. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  159. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  160. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  161. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  162. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  163. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  164. data/lib/grape/validations/validators/values_validator.rb +36 -65
  165. data/lib/grape/validations.rb +8 -21
  166. data/lib/grape/version.rb +1 -2
  167. data/lib/grape/xml.rb +17 -0
  168. data/lib/grape.rb +96 -288
  169. metadata +83 -294
  170. data/lib/grape/api/helpers.rb +0 -9
  171. data/lib/grape/dsl/api.rb +0 -19
  172. data/lib/grape/dsl/configuration.rb +0 -15
  173. data/lib/grape/eager_load.rb +0 -20
  174. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  175. data/lib/grape/exceptions/missing_option.rb +0 -11
  176. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  177. data/lib/grape/exceptions/unknown_options.rb +0 -11
  178. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -27
  179. data/lib/grape/extensions/hash.rb +0 -22
  180. data/lib/grape/extensions/hashie/mash.rb +0 -26
  181. data/lib/grape/http/headers.rb +0 -61
  182. data/lib/grape/middleware/globals.rb +0 -16
  183. data/lib/grape/middleware/helpers.rb +0 -12
  184. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  185. data/lib/grape/router/attribute_translator.rb +0 -63
  186. data/lib/grape/types/invalid_value.rb +0 -8
  187. data/lib/grape/util/base_inheritable.rb +0 -43
  188. data/lib/grape/util/inheritable_values.rb +0 -31
  189. data/lib/grape/util/json.rb +0 -12
  190. data/lib/grape/util/lazy_block.rb +0 -27
  191. data/lib/grape/util/lazy_object.rb +0 -43
  192. data/lib/grape/util/lazy_value.rb +0 -91
  193. data/lib/grape/util/registrable.rb +0 -15
  194. data/lib/grape/util/reverse_stackable_values.rb +0 -20
  195. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  196. data/lib/grape/util/xml.rb +0 -10
  197. data/lib/grape/validations/attributes_doc.rb +0 -58
  198. data/lib/grape/validations/types/build_coercer.rb +0 -94
  199. data/lib/grape/validations/validator_factory.rb +0 -15
  200. data/spec/grape/api/custom_validations_spec.rb +0 -213
  201. data/spec/grape/api/deeply_included_options_spec.rb +0 -56
  202. data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
  203. data/spec/grape/api/documentation_spec.rb +0 -59
  204. data/spec/grape/api/inherited_helpers_spec.rb +0 -114
  205. data/spec/grape/api/instance_spec.rb +0 -103
  206. data/spec/grape/api/invalid_format_spec.rb +0 -45
  207. data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
  208. data/spec/grape/api/nested_helpers_spec.rb +0 -50
  209. data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
  210. data/spec/grape/api/parameters_modification_spec.rb +0 -41
  211. data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
  212. data/spec/grape/api/recognize_path_spec.rb +0 -21
  213. data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
  214. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
  215. data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
  216. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
  217. data/spec/grape/api/shared_helpers_spec.rb +0 -36
  218. data/spec/grape/api_remount_spec.rb +0 -509
  219. data/spec/grape/api_spec.rb +0 -4356
  220. data/spec/grape/dsl/callbacks_spec.rb +0 -45
  221. data/spec/grape/dsl/desc_spec.rb +0 -98
  222. data/spec/grape/dsl/headers_spec.rb +0 -62
  223. data/spec/grape/dsl/helpers_spec.rb +0 -100
  224. data/spec/grape/dsl/inside_route_spec.rb +0 -531
  225. data/spec/grape/dsl/logger_spec.rb +0 -24
  226. data/spec/grape/dsl/middleware_spec.rb +0 -60
  227. data/spec/grape/dsl/parameters_spec.rb +0 -180
  228. data/spec/grape/dsl/request_response_spec.rb +0 -225
  229. data/spec/grape/dsl/routing_spec.rb +0 -275
  230. data/spec/grape/dsl/settings_spec.rb +0 -261
  231. data/spec/grape/dsl/validations_spec.rb +0 -55
  232. data/spec/grape/endpoint/declared_spec.rb +0 -846
  233. data/spec/grape/endpoint_spec.rb +0 -1085
  234. data/spec/grape/entity_spec.rb +0 -336
  235. data/spec/grape/exceptions/base_spec.rb +0 -81
  236. data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -185
  237. data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
  238. data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
  239. data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
  240. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
  241. data/spec/grape/exceptions/missing_group_type_spec.rb +0 -17
  242. data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
  243. data/spec/grape/exceptions/missing_option_spec.rb +0 -15
  244. data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
  245. data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
  246. data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -19
  247. data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
  248. data/spec/grape/exceptions/validation_spec.rb +0 -19
  249. data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
  250. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
  251. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
  252. data/spec/grape/grape_spec.rb +0 -9
  253. data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
  254. data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
  255. data/spec/grape/integration/rack_spec.rb +0 -51
  256. data/spec/grape/loading_spec.rb +0 -44
  257. data/spec/grape/middleware/auth/base_spec.rb +0 -31
  258. data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
  259. data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
  260. data/spec/grape/middleware/base_spec.rb +0 -221
  261. data/spec/grape/middleware/error_spec.rb +0 -85
  262. data/spec/grape/middleware/exception_spec.rb +0 -294
  263. data/spec/grape/middleware/formatter_spec.rb +0 -461
  264. data/spec/grape/middleware/globals_spec.rb +0 -30
  265. data/spec/grape/middleware/stack_spec.rb +0 -155
  266. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
  267. data/spec/grape/middleware/versioner/header_spec.rb +0 -345
  268. data/spec/grape/middleware/versioner/param_spec.rb +0 -171
  269. data/spec/grape/middleware/versioner/path_spec.rb +0 -62
  270. data/spec/grape/middleware/versioner_spec.rb +0 -21
  271. data/spec/grape/named_api_spec.rb +0 -19
  272. data/spec/grape/parser_spec.rb +0 -86
  273. data/spec/grape/path_spec.rb +0 -252
  274. data/spec/grape/presenters/presenter_spec.rb +0 -71
  275. data/spec/grape/request_spec.rb +0 -126
  276. data/spec/grape/util/inheritable_setting_spec.rb +0 -242
  277. data/spec/grape/util/inheritable_values_spec.rb +0 -79
  278. data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
  279. data/spec/grape/util/stackable_values_spec.rb +0 -128
  280. data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
  281. data/spec/grape/validations/attributes_doc_spec.rb +0 -153
  282. data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
  283. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -38
  284. data/spec/grape/validations/params_scope_spec.rb +0 -1420
  285. data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -56
  286. data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
  287. data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
  288. data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
  289. data/spec/grape/validations/types_spec.rb +0 -111
  290. data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
  291. data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
  292. data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
  293. data/spec/grape/validations/validators/base_spec.rb +0 -38
  294. data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
  295. data/spec/grape/validations/validators/default_spec.rb +0 -463
  296. data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
  297. data/spec/grape/validations/validators/except_values_spec.rb +0 -192
  298. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
  299. data/spec/grape/validations/validators/presence_spec.rb +0 -315
  300. data/spec/grape/validations/validators/regexp_spec.rb +0 -161
  301. data/spec/grape/validations/validators/same_as_spec.rb +0 -57
  302. data/spec/grape/validations/validators/values_spec.rb +0 -733
  303. data/spec/grape/validations/validators/zh-CN.yml +0 -10
  304. data/spec/grape/validations_spec.rb +0 -2030
  305. data/spec/integration/eager_load/eager_load_spec.rb +0 -15
  306. data/spec/integration/multi_json/json_spec.rb +0 -7
  307. data/spec/integration/multi_xml/xml_spec.rb +0 -7
  308. data/spec/shared/deprecated_class_examples.rb +0 -16
  309. data/spec/shared/versioning_examples.rb +0 -215
  310. data/spec/spec_helper.rb +0 -52
  311. data/spec/support/basic_auth_encode_helpers.rb +0 -11
  312. data/spec/support/chunks.rb +0 -14
  313. data/spec/support/content_type_helpers.rb +0 -15
  314. data/spec/support/endpoint_faker.rb +0 -25
  315. data/spec/support/file_streamer.rb +0 -13
  316. data/spec/support/integer_helpers.rb +0 -13
  317. data/spec/support/versioned_helpers.rb +0 -55
@@ -1,18 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Formatter < Base
8
- CHUNKED = 'chunked'
6
+ include PrecomputedContentTypes
9
7
 
10
- def default_options
11
- {
12
- default_format: :txt,
13
- formatters: {},
14
- parsers: {}
15
- }
8
+ Options = Data.define(:content_types, :default_format, :format, :formatters, :parsers) do
9
+ def initialize(content_types: nil, default_format: :txt, format: nil, formatters: nil, parsers: nil)
10
+ super
11
+ end
12
+ end
13
+
14
+ ALL_MEDIA_TYPES = '*/*'
15
+
16
+ # The request methods that can carry a body worth parsing. See
17
+ # {#read_body_input?}, which tests the env against this before anything
18
+ # asks for a Rack::Request. QUERY is here because its content *is* the
19
+ # query (RFC 10008, Section 2), not an optional payload.
20
+ BODY_CARRYING_METHODS = [Rack::POST, Rack::PUT, Rack::PATCH, Rack::DELETE, Grape::QUERY].freeze
21
+
22
+ # Read off ivars rather than delegated into +config+ on every request:
23
+ # +negotiate_content_type+ asks for +format+ and +default_format+ per
24
+ # request and +fetch_formatter+ for +formatters+, and each delegator cost
25
+ # a Forwardable frame plus a Data reader for a value that was frozen when
26
+ # the middleware was built.
27
+ attr_reader :default_format, :format, :formatters, :parsers
28
+
29
+ # The formatter is the only middleware that maps an incoming media type
30
+ # back to a format, so it warms +mime_types+ itself rather than making
31
+ # every content-type-aware middleware build a table none of them read.
32
+ def initialize(app, **options)
33
+ super
34
+ @default_format = config.default_format
35
+ @format = config.format
36
+ @formatters = config.formatters
37
+ @parsers = config.parsers
38
+ mime_types
16
39
  end
17
40
 
18
41
  def before
@@ -23,19 +46,17 @@ module Grape
23
46
  def after
24
47
  return unless @app_response
25
48
 
26
- status, headers, bodies = *@app_response
49
+ status, headers, bodies = @app_response
27
50
 
28
- if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
29
- @app_response
30
- else
31
- build_formatted_response(status, headers, bodies)
32
- end
51
+ return [status, headers, []] if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
52
+
53
+ build_formatted_response(status, headers, bodies)
33
54
  end
34
55
 
35
56
  private
36
57
 
37
58
  def build_formatted_response(status, headers, bodies)
38
- headers = ensure_content_type(headers)
59
+ ensure_content_type!(headers)
39
60
 
40
61
  if bodies.is_a?(Grape::ServeStream::StreamResponse)
41
62
  Grape::ServeStream::SendfileResponse.new([], status, headers) do |resp|
@@ -43,136 +64,173 @@ module Grape
43
64
  end
44
65
  else
45
66
  # Allow content-type to be explicitly overwritten
46
- formatter = fetch_formatter(headers, options)
47
- bodymap = ActiveSupport::Notifications.instrument('format_response.grape', formatter: formatter, env: env) do
48
- bodies.collect { |body| formatter.call(body, env) }
67
+ formatter = fetch_formatter(headers)
68
+ bodymap = instrument_format_response(formatter) do
69
+ bodies.map { |body| formatter.call(body, env) }
49
70
  end
50
- Rack::Response.new(bodymap, status, headers)
71
+ # A bare Rack tuple rather than a Rack::Response: +headers+ is already
72
+ # a Grape::Util::Header (a Rack::Headers on Rack 3), so wrapping only
73
+ # re-normalizes the same keys into a second Headers hash that
74
+ # +Middleware::Base#call+ unwraps again with +to_a+ on the way out.
75
+ # The 204/304 bodies Rack::Response#finish would blank are returned
76
+ # above, before this point.
77
+ [status, headers, bodymap]
51
78
  end
52
79
  rescue Grape::Exceptions::InvalidFormatter => e
53
- throw :error, status: 500, message: e.message, backtrace: e.backtrace, original_exception: e
80
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 500, message: e.message, backtrace: e.backtrace, original_exception: e)
54
81
  end
55
82
 
56
- def fetch_formatter(headers, options)
57
- api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
58
- Grape::Formatter.formatter_for(api_format, **options)
83
+ # Guards on +listening?+ so that with no subscriber the payload Hash and
84
+ # notification machinery are skipped and the block runs directly (no added
85
+ # allocations); the block is forwarded anonymously.
86
+ def instrument_format_response(formatter, &)
87
+ return yield unless ActiveSupport::Notifications.notifier.listening?('format_response.grape')
88
+
89
+ ActiveSupport::Notifications.instrument('format_response.grape', formatter:, env:, &)
90
+ end
91
+
92
+ def fetch_formatter(headers)
93
+ api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
94
+ Grape::Formatter.formatter_for(api_format, formatters)
59
95
  end
60
96
 
61
97
  # Set the content type header for the API format if it is not already present.
62
98
  #
63
- # @param headers [Hash]
64
- # @return [Hash]
65
- def ensure_content_type(headers)
66
- if headers[Grape::Http::Headers::CONTENT_TYPE]
67
- headers
68
- else
69
- headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
70
- end
71
- end
99
+ # Written into +headers+ rather than returned as a copy, hence the +!+:
100
+ # this runs on every response, and copying the hash costs an allocation
101
+ # per request for a header the caller is about to send anyway.
102
+ #
103
+ # @param headers [Hash] the response headers, mutated in place
104
+ # @return [void]
105
+ def ensure_content_type!(headers)
106
+ return if headers[Rack::CONTENT_TYPE]
72
107
 
73
- def request
74
- @request ||= Rack::Request.new(env)
108
+ headers[Rack::CONTENT_TYPE] = content_type_for(env[Grape::Env::API_FORMAT])
75
109
  end
76
110
 
77
- # store read input in env['api.request.input']
78
111
  def read_body_input
79
- return unless
80
- (request.post? || request.put? || request.patch? || request.delete?) &&
81
- (!request.form_data? || !request.media_type) &&
82
- !request.parseable_data? &&
83
- (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
112
+ return unless read_body_input?
113
+
114
+ input = rack_request.body # reads RACK_INPUT
115
+ return if input.nil?
84
116
 
85
- return unless (input = env[Grape::Env::RACK_INPUT])
117
+ rewind = input.respond_to?(:rewind)
86
118
 
87
- input.rewind
119
+ input.rewind if rewind
88
120
  body = env[Grape::Env::API_REQUEST_INPUT] = input.read
89
121
  begin
90
- read_rack_input(body) if body && !body.empty?
122
+ read_rack_input(body)
91
123
  ensure
92
- input.rewind
124
+ input.rewind if rewind
93
125
  end
94
126
  end
95
127
 
96
- # store parsed input in env['api.request.body']
97
128
  def read_rack_input(body)
98
- fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
99
-
100
- throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
101
- parser = Grape::Parser.parser_for fmt, **options
102
- if parser
103
- begin
104
- body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
105
- if body.is_a?(Hash)
106
- env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env.key?(Grape::Env::RACK_REQUEST_FORM_HASH)
107
- env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
108
- else
109
- body
110
- end
111
- env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
129
+ return if body.empty?
130
+
131
+ media_type = rack_request.media_type
132
+
133
+ # RFC 10008, Sections 2 and 2.1: a QUERY carries its query in the
134
+ # content, so a request that never says what that content is cannot be
135
+ # interpreted and must fail rather than be read as the default format.
136
+ # Every other method keeps that fallback.
137
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: 'The QUERY method requires a content-type.') if media_type.nil? && env[Rack::REQUEST_METHOD] == Grape::QUERY
138
+
139
+ fmt = media_type ? mime_types[media_type] : default_format
140
+
141
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 415, message: "The provided content-type '#{media_type}' is not supported.") unless content_type_for(fmt)
142
+ parser = Grape::Parser.parser_for fmt, parsers
143
+ return env[Grape::Env::API_REQUEST_BODY] = body unless parser
144
+
145
+ begin
146
+ parsed = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
147
+ if parsed.is_a?(Hash)
148
+ if (form_hash = env[Rack::RACK_REQUEST_FORM_HASH])
149
+ form_hash.merge!(parsed)
150
+ else
151
+ env[Rack::RACK_REQUEST_FORM_HASH] = parsed
112
152
  end
113
- rescue Grape::Exceptions::Base => e
114
- raise e
115
- rescue StandardError => e
116
- throw :error, status: 400, message: e.message, backtrace: e.backtrace, original_exception: e
153
+ env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
117
154
  end
118
- else
119
- env[Grape::Env::API_REQUEST_BODY] = body
155
+ rescue Grape::Exceptions::Base => e
156
+ raise e
157
+ rescue StandardError => e
158
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: e.message, backtrace: e.backtrace, original_exception: e)
120
159
  end
121
160
  end
122
161
 
123
- def negotiate_content_type
124
- fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
125
- if content_type_for(fmt)
126
- env[Grape::Env::API_FORMAT] = fmt
127
- else
128
- throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
129
- end
162
+ # this middleware will not try to format the following content-types since Rack already handles them
163
+ # when calling Rack's `params` function
164
+ # - application/x-www-form-urlencoded
165
+ # - multipart/form-data
166
+ # - multipart/related
167
+ # - multipart/mixed
168
+ def read_body_input?
169
+ # Read off the env rather than through Rack::Request's predicates: this
170
+ # is what decides the question for every request, and on the GET, HEAD
171
+ # and OPTIONS majority it is the only thing the formatter would have
172
+ # built a Rack::Request for.
173
+ return false unless BODY_CARRYING_METHODS.include?(env[Rack::REQUEST_METHOD])
174
+ return false if rack_request.form_data? && rack_request.content_type
175
+ return false if rack_request.parseable_data?
176
+
177
+ rack_request.content_length.to_i.positive? || env['HTTP_TRANSFER_ENCODING'] == 'chunked'
130
178
  end
131
179
 
132
- def format_from_extension
133
- parts = request.path.split('.')
180
+ def negotiate_content_type
181
+ fmt = format_from_extension || format_from_query || format || format_from_header || default_format
182
+ return env[Grape::Env::API_FORMAT] = fmt.to_sym if content_type_for(fmt)
134
183
 
135
- if parts.size > 1
136
- extension = parts.last
137
- # avoid symbol memory leak on an unknown format
138
- return extension.to_sym if content_type_for(extension)
139
- end
140
- nil
184
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 406, message: "The requested format '#{fmt}' is not supported.")
141
185
  end
142
186
 
143
- def format_from_params
144
- fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
145
- # avoid symbol memory leak on an unknown format
146
- return fmt.to_sym if content_type_for(fmt)
187
+ # Only the extension is scrubbed, and only once the path turns out to have
188
+ # one: +String#rindex+ takes a byte offset and never raises on an invalid
189
+ # sequence, and a +.+ byte cannot be part of a multi-byte one, so the dot
190
+ # sits at the same place before and after scrubbing. The overwhelming
191
+ # majority of paths carry no extension and now skip the scrub entirely.
192
+ def format_from_extension
193
+ request_path = path_for_extension
194
+ dot_pos = request_path.rindex('.')
195
+ return unless dot_pos
147
196
 
148
- fmt
197
+ extension = try_scrub(request_path[(dot_pos + 1)..])
198
+ extension if content_type_for(extension)
149
199
  end
150
200
 
151
- def format_from_header
152
- mime_array.each do |t|
153
- return mime_types[t] if mime_types.key?(t)
154
- end
155
- nil
201
+ # The extension is the tail of the request path, so PATH_INFO answers it
202
+ # on its own whenever there is one: a dot in SCRIPT_NAME is followed by
203
+ # the slash that opens PATH_INFO, and no registered extension holds a
204
+ # slash. Only an empty PATH_INFO needs +Rack::Request#path+ — and with it
205
+ # the String its concatenation allocates. Tested with +empty?+ rather
206
+ # than +blank?+: the path is not scrubbed yet, and a regexp match on an
207
+ # invalid byte sequence raises.
208
+ def path_for_extension
209
+ path_info = env[Rack::PATH_INFO]
210
+ return rack_request.path if path_info.nil? || path_info.empty?
211
+
212
+ path_info
156
213
  end
157
214
 
158
- def mime_array
159
- accept = env[Grape::Http::Headers::HTTP_ACCEPT]
160
- return [] unless accept
215
+ # +?format=+ can only be there when there is a query string at all, so
216
+ # the common query-less request skips parsing one.
217
+ def format_from_query
218
+ query_string = env[Rack::QUERY_STRING]
219
+ return if query_string.nil? || query_string.empty?
161
220
 
162
- accept_into_mime_and_quality = %r{
163
- (
164
- \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
165
- (?:
166
- (?:;[^,]*?)? # optionally multiple formats in a row
167
- ;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5)
168
- )?
169
- }x
221
+ query_params['format']
222
+ end
170
223
 
171
- vendor_prefix_pattern = /vnd\.[^+]+\+/
224
+ # Media types are case-insensitive (RFC 9110 §8.3.1) but the registered
225
+ # ones are spelled in lower case and Rack matches them literally, so an
226
+ # `Accept: TEXT/PLAIN` found nothing and fell through to the default
227
+ # format — the client quietly got something other than what it asked for.
228
+ def format_from_header
229
+ accept_header = try_scrub(env['HTTP_ACCEPT'])
230
+ return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES
172
231
 
173
- accept.scan(accept_into_mime_and_quality)
174
- .sort_by { |_, quality_preference| -quality_preference.to_f }
175
- .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
232
+ media_type = Rack::Utils.best_q_match(accept_header.downcase, mime_types.keys)
233
+ mime_types[media_type] if media_type
176
234
  end
177
235
  end
178
236
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ # Include in a middleware subclass that needs content-type negotiation.
6
+ # Provides +content_types+ / +mime_types+ / +content_type_for+ /
7
+ # +content_type+ resolved from +config.content_types+ and
8
+ # +config.format+ — so the consuming middleware's +Options+ Data class
9
+ # must declare both fields. Warms those caches on the parent instance
10
+ # at initialization so per-request +dup+s inherit them rather than
11
+ # rebuilding them.
12
+ #
13
+ # +mime_types+ is not warmed here: Formatter is the only middleware that
14
+ # reads it, and it warms it itself. The tables behind +mime_types+ and
15
+ # +content_type_for+ are shared process-wide per content-type registry
16
+ # (see Grape::ContentTypes), so the ivars below memoize a lookup, not a
17
+ # copy.
18
+ #
19
+ # Opt-in: plain +Grape::Middleware::Base+ subclasses that don't need
20
+ # content-type-aware helpers don't pay for them.
21
+ module PrecomputedContentTypes
22
+ def initialize(app, **options)
23
+ super
24
+ content_types
25
+ content_types_lookup
26
+ end
27
+
28
+ def content_types
29
+ @content_types ||= Grape::ContentTypes.content_types_for(config.content_types)
30
+ end
31
+
32
+ def mime_types
33
+ @mime_types ||= Grape::ContentTypes.mime_types_for(content_types)
34
+ end
35
+
36
+ def content_type_for(format)
37
+ content_types_lookup[format]
38
+ end
39
+
40
+ def content_type
41
+ content_type_for(env[Grape::Env::API_FORMAT] || config.format) || 'text/html'
42
+ end
43
+
44
+ private
45
+
46
+ def content_types_lookup
47
+ @content_types_lookup ||= Grape::ContentTypes.lookup_for(content_types)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -5,10 +5,12 @@ module Grape
5
5
  # Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack
6
6
  # It allows to insert and insert after
7
7
  class Stack
8
+ extend Forwardable
9
+
8
10
  class Middleware
9
11
  attr_reader :args, :block, :klass
10
12
 
11
- def initialize(klass, *args, &block)
13
+ def initialize(klass, args, block)
12
14
  @klass = klass
13
15
  @args = args
14
16
  @block = block
@@ -26,13 +28,27 @@ module Grape
26
28
  klass == other || (name.nil? && klass.superclass == other)
27
29
  end
28
30
  end
31
+ alias eql? ==
32
+
33
+ # Keyed on the wrapped class, so two Middleware wrapping the same
34
+ # class hash alike — and so does the class itself, which #== also
35
+ # accepts. The superclass fallback above can't be honoured here (a
36
+ # class and its superclass hash differently); it only ever serves
37
+ # #index / #include?, which compare with #== rather than by hash.
38
+ def hash
39
+ klass.hash
40
+ end
29
41
 
30
42
  def inspect
31
43
  klass.to_s
32
44
  end
33
45
 
34
- def use_in(builder)
35
- builder.use(@klass, *@args, &@block)
46
+ def build(builder)
47
+ # we need to force the ruby2_keywords_hash for middlewares that initialize contains keywords
48
+ # like ActionDispatch::RequestId since middleware arguments are serialized
49
+ # https://rubyapi.org/3.4/o/hash#method-c-ruby2_keywords_hash
50
+ args[-1] = Hash.ruby2_keywords_hash(args[-1]) if args.last.is_a?(Hash) && Hash.respond_to?(:ruby2_keywords_hash)
51
+ builder.use(klass, *args, &block)
36
52
  end
37
53
  end
38
54
 
@@ -40,73 +56,55 @@ module Grape
40
56
 
41
57
  attr_accessor :middlewares, :others
42
58
 
59
+ def_delegators :middlewares, :each, :size, :last, :[]
60
+
43
61
  def initialize
44
62
  @middlewares = []
45
63
  @others = []
46
64
  end
47
65
 
48
- def each(&block)
49
- @middlewares.each(&block)
50
- end
51
-
52
- def size
53
- middlewares.size
54
- end
55
-
56
- def last
57
- middlewares.last
58
- end
59
-
60
- def [](i)
61
- middlewares[i]
66
+ def insert(index, klass, *args, &block)
67
+ at = assert_index(index, :before)
68
+ middlewares.insert(at, self.class::Middleware.new(klass, args, block))
62
69
  end
63
70
 
64
- def insert(index, *args, &block)
65
- index = assert_index(index, :before)
66
- middleware = self.class::Middleware.new(*args, &block)
67
- middlewares.insert(index, middleware)
68
- end
69
- ruby2_keywords :insert if respond_to?(:ruby2_keywords, true)
70
-
71
71
  alias insert_before insert
72
72
 
73
- def insert_after(index, *args, &block)
74
- index = assert_index(index, :after)
75
- insert(index + 1, *args, &block)
73
+ def insert_after(index, ...)
74
+ at = assert_index(index, :after)
75
+ insert(at + 1, ...)
76
76
  end
77
- ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
78
77
 
79
- def use(*args, &block)
80
- middleware = self.class::Middleware.new(*args, &block)
78
+ def use(klass, *args, &block)
79
+ middleware = self.class::Middleware.new(klass, args, block)
81
80
  middlewares.push(middleware)
82
81
  end
83
- ruby2_keywords :use if respond_to?(:ruby2_keywords, true)
84
82
 
85
83
  def merge_with(middleware_specs)
86
- middleware_specs.each do |operation, *args|
84
+ middleware_specs.each do |operation, klass, *args|
87
85
  if args.last.is_a?(Proc)
88
86
  last_proc = args.pop
89
- public_send(operation, *args, &last_proc)
87
+ public_send(operation, klass, *args, &last_proc)
90
88
  else
91
- public_send(operation, *args)
89
+ public_send(operation, klass, *args)
92
90
  end
93
91
  end
94
92
  end
95
93
 
96
94
  # @return [Rack::Builder] the builder object with our middlewares applied
97
- def build(builder = Rack::Builder.new)
95
+ def build
96
+ builder = Rack::Builder.new
98
97
  others.shift(others.size).each { |m| merge_with(m) }
99
- middlewares.each do |m|
100
- m.use_in(builder)
101
- end
98
+ middlewares.each { |m| m.build(builder) }
102
99
  builder
103
100
  end
104
101
 
105
102
  # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
106
103
  # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
107
104
  def concat(other_specs)
108
- @others << Array(other_specs).reject { |o| o.first == :use }
109
- merge_with(Array(other_specs).select { |o| o.first == :use })
105
+ use, not_use = other_specs.partition { |o| o.first == :use }
106
+ others << not_use
107
+ merge_with(use)
110
108
  end
111
109
 
112
110
  protected
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  module Versioner
@@ -20,44 +18,19 @@ module Grape
20
18
  # route.
21
19
  class AcceptVersionHeader < Base
22
20
  def before
23
- potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
24
-
25
- if strict? && potential_version.empty?
26
- # If no Accept-Version header:
27
- throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
28
- end
29
-
30
- return if potential_version.empty?
21
+ potential_version = try_scrub(env['HTTP_ACCEPT_VERSION'])
22
+ not_acceptable!('Accept-Version header must be set.') if strict && potential_version.blank?
31
23
 
32
- # If the requested version is not supported:
33
- throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version }
24
+ return if potential_version.blank?
34
25
 
26
+ not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version)
35
27
  env[Grape::Env::API_VERSION] = potential_version
36
28
  end
37
29
 
38
30
  private
39
31
 
40
- def versions
41
- options[:versions] || []
42
- end
43
-
44
- def strict?
45
- options[:version_options] && options[:version_options][:strict]
46
- end
47
-
48
- # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
49
- # of routes (see Grape::Router) for more information). To prevent
50
- # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
51
- def cascade?
52
- if options[:version_options]&.key?(:cascade)
53
- options[:version_options][:cascade]
54
- else
55
- true
56
- end
57
- end
58
-
59
- def error_headers
60
- cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
32
+ def not_acceptable!(message)
33
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 406, headers: error_headers, message:)
61
34
  end
62
35
  end
63
36
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ class Base < Grape::Middleware::Base
7
+ extend Forwardable
8
+ include Grape::Middleware::PrecomputedContentTypes
9
+
10
+ Options = Data.define(
11
+ :content_types, :format, :mount_path, :prefix, :version_options, :versions
12
+ ) do
13
+ def initialize(
14
+ content_types: nil, format: nil, mount_path: nil, prefix: nil,
15
+ version_options: Grape::DSL::VersionOptions.new, versions: nil
16
+ )
17
+ super
18
+ end
19
+ end
20
+
21
+ CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze
22
+
23
+ def self.inherited(klass)
24
+ super
25
+ Versioner.register(klass)
26
+ end
27
+
28
+ attr_reader :available_media_types, :error_headers, :versions
29
+
30
+ def_delegators :config, :mount_path, :prefix, :version_options
31
+ def_delegators :version_options, :cascade, :parameter, :strict, :vendor
32
+
33
+ def initialize(app, **options)
34
+ super
35
+ @versions = config.versions&.map(&:to_s) # making sure versions are strings to ease potential match
36
+ @error_headers = cascade ? CASCADE_PASS_HEADER : {}
37
+ @available_media_types = build_available_media_types
38
+ end
39
+
40
+ def potential_version_match?(potential_version)
41
+ versions.blank? || versions.include?(potential_version)
42
+ end
43
+
44
+ def version_not_found!
45
+ throw :error, Grape::Exceptions::ErrorResponse.new(status: 404, message: '404 API Version Not Found', headers: CASCADE_PASS_HEADER)
46
+ end
47
+
48
+ private
49
+
50
+ def build_available_media_types
51
+ media_types = []
52
+ base_media_type = "application/vnd.#{vendor}"
53
+ versions&.reverse_each do |version|
54
+ versioned_base = "#{base_media_type}-#{version}"
55
+ content_types.each_key { |extension| media_types << "#{versioned_base}+#{extension}" }
56
+ media_types << versioned_base
57
+ end
58
+ content_types.each_key { |extension| media_types << "#{base_media_type}+#{extension}" }
59
+ media_types << base_media_type
60
+ media_types.concat(content_types.values.flatten)
61
+ media_types
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end