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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +517 -1
- data/CONTRIBUTING.md +19 -11
- data/README.md +841 -545
- data/UPGRADING.md +1553 -7
- data/grape.gemspec +11 -14
- data/lib/grape/api/instance.rb +77 -160
- data/lib/grape/api.rb +75 -107
- data/lib/grape/content_types.rb +67 -9
- data/lib/grape/cookies.rb +28 -25
- data/lib/grape/declared_params_handler.rb +116 -0
- data/lib/grape/dry_types.rb +48 -6
- data/lib/grape/dsl/callbacks.rb +8 -58
- data/lib/grape/dsl/declared.rb +35 -0
- data/lib/grape/dsl/desc.rb +25 -63
- data/lib/grape/dsl/entity.rb +106 -0
- data/lib/grape/dsl/headers.rb +2 -2
- data/lib/grape/dsl/helpers.rb +83 -64
- data/lib/grape/dsl/inside_route.rb +69 -296
- data/lib/grape/dsl/logger.rb +4 -9
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +88 -94
- data/lib/grape/dsl/request_response.rb +143 -147
- data/lib/grape/dsl/rescue_options.rb +25 -0
- data/lib/grape/dsl/routing.rb +333 -191
- data/lib/grape/dsl/settings.rb +29 -135
- data/lib/grape/dsl/validations.rb +39 -32
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +25 -0
- data/lib/grape/endpoint.rb +319 -269
- data/lib/grape/{util/env.rb → env.rb} +8 -8
- data/lib/grape/error_formatter/base.rb +57 -21
- data/lib/grape/error_formatter/json.rb +13 -15
- data/lib/grape/error_formatter/serializable_hash.rb +7 -0
- data/lib/grape/error_formatter/txt.rb +12 -18
- data/lib/grape/error_formatter/xml.rb +3 -13
- data/lib/grape/error_formatter.rb +9 -25
- data/lib/grape/exceptions/base.rb +22 -58
- data/lib/grape/exceptions/error_response.rb +48 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_message_body.rb +1 -1
- data/lib/grape/exceptions/invalid_version_header.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/method_not_allowed.rb +1 -1
- data/lib/grape/exceptions/missing_group_type.rb +0 -2
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/request_error.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/unknown_parameter.rb +1 -1
- data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
- data/lib/grape/exceptions/validation.rb +28 -11
- data/lib/grape/exceptions/validation_array_errors.rb +5 -0
- data/lib/grape/exceptions/validation_errors.rb +22 -26
- data/lib/grape/formatter/base.rb +16 -0
- data/lib/grape/formatter/json.rb +5 -6
- data/lib/grape/formatter/serializable_hash.rb +7 -10
- data/lib/grape/formatter/txt.rb +3 -5
- data/lib/grape/formatter/xml.rb +4 -6
- data/lib/grape/formatter.rb +7 -25
- data/lib/grape/json.rb +46 -0
- data/lib/grape/locale/en.yml +44 -42
- data/lib/grape/middleware/auth/base.rb +11 -33
- data/lib/grape/middleware/auth/dsl.rb +12 -34
- data/lib/grape/middleware/auth/strategies.rb +1 -2
- data/lib/grape/middleware/base.rb +56 -33
- data/lib/grape/middleware/error.rb +290 -91
- data/lib/grape/middleware/formatter.rb +164 -106
- data/lib/grape/middleware/precomputed_content_types.rb +51 -0
- data/lib/grape/middleware/stack.rb +38 -40
- data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
- data/lib/grape/middleware/versioner/base.rb +66 -0
- data/lib/grape/middleware/versioner/header.rb +44 -129
- data/lib/grape/middleware/versioner/param.rb +4 -25
- data/lib/grape/middleware/versioner/path.rb +48 -25
- data/lib/grape/middleware/versioner.rb +7 -14
- data/lib/grape/mountable.rb +22 -0
- data/lib/grape/namespace.rb +21 -14
- data/lib/grape/params_builder/base.rb +20 -0
- data/lib/grape/params_builder/hash.rb +11 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
- data/lib/grape/params_builder/hashie_mash.rb +11 -0
- data/lib/grape/params_builder.rb +15 -0
- data/lib/grape/parser/base.rb +16 -0
- data/lib/grape/parser/json.rb +6 -8
- data/lib/grape/parser/xml.rb +6 -8
- data/lib/grape/parser.rb +5 -23
- data/lib/grape/path.rb +8 -94
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/railtie.rb +9 -0
- data/lib/grape/request.rb +200 -26
- data/lib/grape/router/base_route.rb +85 -0
- data/lib/grape/router/greedy_route.rb +30 -0
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +77 -35
- data/lib/grape/router/route.rb +79 -60
- data/lib/grape/router.rb +128 -104
- data/lib/grape/serve_stream/file_body.rb +7 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +7 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/api_description.rb +67 -0
- data/lib/grape/util/cache.rb +22 -5
- data/lib/grape/util/deep_freeze.rb +34 -0
- data/lib/grape/util/endpoint_configuration.rb +1 -1
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/header.rb +13 -0
- data/lib/grape/util/inheritable_setting.rb +777 -39
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +22 -0
- data/lib/grape/util/lazy/value.rb +31 -0
- data/lib/grape/util/lazy/value_array.rb +21 -0
- data/lib/grape/util/lazy/value_enumerable.rb +31 -0
- data/lib/grape/util/lazy/value_hash.rb +21 -0
- data/lib/grape/util/media_type.rb +74 -0
- data/lib/grape/util/path_normalizer.rb +37 -0
- data/lib/grape/util/registry.rb +37 -0
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +40 -16
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +61 -28
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/contract_scope.rb +29 -0
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/oneof_collector.rb +35 -0
- data/lib/grape/validations/param_scope_tracker.rb +62 -0
- data/lib/grape/validations/params_documentation.rb +52 -0
- data/lib/grape/validations/params_scope.rb +223 -306
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/single_attribute_iterator.rb +6 -4
- data/lib/grape/validations/types/array_coercer.rb +7 -12
- data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +17 -28
- data/lib/grape/validations/types/json.rb +1 -5
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +14 -35
- data/lib/grape/validations/types/set_coercer.rb +1 -4
- data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
- data/lib/grape/validations/types.rb +29 -54
- data/lib/grape/validations/validations_spec.rb +164 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
- data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
- data/lib/grape/validations/validators/base.rb +118 -37
- data/lib/grape/validations/validators/coerce_validator.rb +26 -38
- data/lib/grape/validations/validators/contract_scope_validator.rb +46 -0
- data/lib/grape/validations/validators/default_validator.rb +13 -16
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +15 -5
- data/lib/grape/validations/validators/length_validator.rb +50 -0
- data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
- data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +51 -0
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +11 -3
- data/lib/grape/validations/validators/same_as_validator.rb +7 -15
- data/lib/grape/validations/validators/values_validator.rb +36 -65
- data/lib/grape/validations.rb +8 -21
- data/lib/grape/version.rb +1 -2
- data/lib/grape/xml.rb +17 -0
- data/lib/grape.rb +96 -288
- metadata +83 -294
- data/lib/grape/api/helpers.rb +0 -9
- data/lib/grape/dsl/api.rb +0 -19
- data/lib/grape/dsl/configuration.rb +0 -15
- data/lib/grape/eager_load.rb +0 -20
- data/lib/grape/exceptions/empty_message_body.rb +0 -11
- data/lib/grape/exceptions/missing_option.rb +0 -11
- data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
- data/lib/grape/exceptions/unknown_options.rb +0 -11
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -27
- data/lib/grape/extensions/hash.rb +0 -22
- data/lib/grape/extensions/hashie/mash.rb +0 -26
- data/lib/grape/http/headers.rb +0 -61
- data/lib/grape/middleware/globals.rb +0 -16
- data/lib/grape/middleware/helpers.rb +0 -12
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
- data/lib/grape/router/attribute_translator.rb +0 -63
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -31
- data/lib/grape/util/json.rb +0 -12
- data/lib/grape/util/lazy_block.rb +0 -27
- data/lib/grape/util/lazy_object.rb +0 -43
- data/lib/grape/util/lazy_value.rb +0 -91
- data/lib/grape/util/registrable.rb +0 -15
- data/lib/grape/util/reverse_stackable_values.rb +0 -20
- data/lib/grape/util/strict_hash_configuration.rb +0 -108
- data/lib/grape/util/xml.rb +0 -10
- data/lib/grape/validations/attributes_doc.rb +0 -58
- data/lib/grape/validations/types/build_coercer.rb +0 -94
- data/lib/grape/validations/validator_factory.rb +0 -15
- data/spec/grape/api/custom_validations_spec.rb +0 -213
- data/spec/grape/api/deeply_included_options_spec.rb +0 -56
- data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -38
- data/spec/grape/api/documentation_spec.rb +0 -59
- data/spec/grape/api/inherited_helpers_spec.rb +0 -114
- data/spec/grape/api/instance_spec.rb +0 -103
- data/spec/grape/api/invalid_format_spec.rb +0 -45
- data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -38
- data/spec/grape/api/nested_helpers_spec.rb +0 -50
- data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -43
- data/spec/grape/api/parameters_modification_spec.rb +0 -41
- data/spec/grape/api/patch_method_helpers_spec.rb +0 -79
- data/spec/grape/api/recognize_path_spec.rb +0 -21
- data/spec/grape/api/required_parameters_in_route_spec.rb +0 -37
- data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -26
- data/spec/grape/api/routes_with_requirements_spec.rb +0 -59
- data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -41
- data/spec/grape/api/shared_helpers_spec.rb +0 -36
- data/spec/grape/api_remount_spec.rb +0 -509
- data/spec/grape/api_spec.rb +0 -4356
- data/spec/grape/dsl/callbacks_spec.rb +0 -45
- data/spec/grape/dsl/desc_spec.rb +0 -98
- data/spec/grape/dsl/headers_spec.rb +0 -62
- data/spec/grape/dsl/helpers_spec.rb +0 -100
- data/spec/grape/dsl/inside_route_spec.rb +0 -531
- data/spec/grape/dsl/logger_spec.rb +0 -24
- data/spec/grape/dsl/middleware_spec.rb +0 -60
- data/spec/grape/dsl/parameters_spec.rb +0 -180
- data/spec/grape/dsl/request_response_spec.rb +0 -225
- data/spec/grape/dsl/routing_spec.rb +0 -275
- data/spec/grape/dsl/settings_spec.rb +0 -261
- data/spec/grape/dsl/validations_spec.rb +0 -55
- data/spec/grape/endpoint/declared_spec.rb +0 -846
- data/spec/grape/endpoint_spec.rb +0 -1085
- data/spec/grape/entity_spec.rb +0 -336
- data/spec/grape/exceptions/base_spec.rb +0 -81
- data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -185
- data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -358
- data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -15
- data/spec/grape/exceptions/invalid_response_spec.rb +0 -11
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +0 -15
- data/spec/grape/exceptions/missing_group_type_spec.rb +0 -17
- data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -17
- data/spec/grape/exceptions/missing_option_spec.rb +0 -15
- data/spec/grape/exceptions/unknown_options_spec.rb +0 -15
- data/spec/grape/exceptions/unknown_validator_spec.rb +0 -15
- data/spec/grape/exceptions/unsupported_group_type_spec.rb +0 -19
- data/spec/grape/exceptions/validation_errors_spec.rb +0 -92
- data/spec/grape/exceptions/validation_spec.rb +0 -19
- data/spec/grape/extensions/param_builders/hash_spec.rb +0 -83
- data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -105
- data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -79
- data/spec/grape/grape_spec.rb +0 -9
- data/spec/grape/integration/global_namespace_function_spec.rb +0 -29
- data/spec/grape/integration/rack_sendfile_spec.rb +0 -48
- data/spec/grape/integration/rack_spec.rb +0 -51
- data/spec/grape/loading_spec.rb +0 -44
- data/spec/grape/middleware/auth/base_spec.rb +0 -31
- data/spec/grape/middleware/auth/dsl_spec.rb +0 -60
- data/spec/grape/middleware/auth/strategies_spec.rb +0 -120
- data/spec/grape/middleware/base_spec.rb +0 -221
- data/spec/grape/middleware/error_spec.rb +0 -85
- data/spec/grape/middleware/exception_spec.rb +0 -294
- data/spec/grape/middleware/formatter_spec.rb +0 -461
- data/spec/grape/middleware/globals_spec.rb +0 -30
- data/spec/grape/middleware/stack_spec.rb +0 -155
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -122
- data/spec/grape/middleware/versioner/header_spec.rb +0 -345
- data/spec/grape/middleware/versioner/param_spec.rb +0 -171
- data/spec/grape/middleware/versioner/path_spec.rb +0 -62
- data/spec/grape/middleware/versioner_spec.rb +0 -21
- data/spec/grape/named_api_spec.rb +0 -19
- data/spec/grape/parser_spec.rb +0 -86
- data/spec/grape/path_spec.rb +0 -252
- data/spec/grape/presenters/presenter_spec.rb +0 -71
- data/spec/grape/request_spec.rb +0 -126
- data/spec/grape/util/inheritable_setting_spec.rb +0 -242
- data/spec/grape/util/inheritable_values_spec.rb +0 -79
- data/spec/grape/util/reverse_stackable_values_spec.rb +0 -134
- data/spec/grape/util/stackable_values_spec.rb +0 -128
- data/spec/grape/util/strict_hash_configuration_spec.rb +0 -38
- data/spec/grape/validations/attributes_doc_spec.rb +0 -153
- data/spec/grape/validations/instance_behaivour_spec.rb +0 -43
- data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -38
- data/spec/grape/validations/params_scope_spec.rb +0 -1420
- data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -56
- data/spec/grape/validations/types/array_coercer_spec.rb +0 -33
- data/spec/grape/validations/types/primitive_coercer_spec.rb +0 -150
- data/spec/grape/validations/types/set_coercer_spec.rb +0 -32
- data/spec/grape/validations/types_spec.rb +0 -111
- data/spec/grape/validations/validators/all_or_none_spec.rb +0 -162
- data/spec/grape/validations/validators/allow_blank_spec.rb +0 -575
- data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -205
- data/spec/grape/validations/validators/base_spec.rb +0 -38
- data/spec/grape/validations/validators/coerce_spec.rb +0 -1261
- data/spec/grape/validations/validators/default_spec.rb +0 -463
- data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -233
- data/spec/grape/validations/validators/except_values_spec.rb +0 -192
- data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -214
- data/spec/grape/validations/validators/presence_spec.rb +0 -315
- data/spec/grape/validations/validators/regexp_spec.rb +0 -161
- data/spec/grape/validations/validators/same_as_spec.rb +0 -57
- data/spec/grape/validations/validators/values_spec.rb +0 -733
- data/spec/grape/validations/validators/zh-CN.yml +0 -10
- data/spec/grape/validations_spec.rb +0 -2030
- data/spec/integration/eager_load/eager_load_spec.rb +0 -15
- data/spec/integration/multi_json/json_spec.rb +0 -7
- data/spec/integration/multi_xml/xml_spec.rb +0 -7
- data/spec/shared/deprecated_class_examples.rb +0 -16
- data/spec/shared/versioning_examples.rb +0 -215
- data/spec/spec_helper.rb +0 -52
- data/spec/support/basic_auth_encode_helpers.rb +0 -11
- data/spec/support/chunks.rb +0 -14
- data/spec/support/content_type_helpers.rb +0 -15
- data/spec/support/endpoint_faker.rb +0 -25
- data/spec/support/file_streamer.rb +0 -13
- data/spec/support/integer_helpers.rb +0 -13
- 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
|
-
|
|
6
|
+
include PrecomputedContentTypes
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 =
|
|
49
|
+
status, headers, bodies = @app_response
|
|
27
50
|
|
|
28
|
-
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
bodymap =
|
|
48
|
-
bodies.
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
#
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
197
|
+
extension = try_scrub(request_path[(dot_pos + 1)..])
|
|
198
|
+
extension if content_type_for(extension)
|
|
149
199
|
end
|
|
150
200
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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,
|
|
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
|
|
35
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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,
|
|
74
|
-
|
|
75
|
-
insert(
|
|
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(
|
|
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
|
|
95
|
+
def build
|
|
96
|
+
builder = Rack::Builder.new
|
|
98
97
|
others.shift(others.size).each { |m| merge_with(m) }
|
|
99
|
-
middlewares.each
|
|
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
|
-
|
|
109
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
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
|