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
data/lib/grape/router/pattern.rb
CHANGED
|
@@ -1,62 +1,104 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'forwardable'
|
|
4
|
-
require 'mustermann/grape'
|
|
5
|
-
require 'grape/util/cache'
|
|
6
|
-
|
|
7
3
|
module Grape
|
|
8
4
|
class Router
|
|
9
5
|
class Pattern
|
|
10
|
-
|
|
11
|
-
DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze
|
|
6
|
+
extend Forwardable
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
DEFAULT_CAPTURES = %w[format version].freeze
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
attr_reader :origin, :path, :pattern, :to_regexp, :anchor, :version, :requirements
|
|
11
|
+
|
|
12
|
+
def_delegators :pattern, :params
|
|
17
13
|
def_delegators :to_regexp, :===
|
|
18
14
|
alias match? ===
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
# Build a Pattern from a raw path, namespace and the API's inheritable
|
|
17
|
+
# settings. {Path} owns the settings-aware assembly of +origin+/+suffix+;
|
|
18
|
+
# the Pattern itself stays value-based (see {#initialize}).
|
|
19
|
+
def self.build(path:, namespace:, settings:, anchor:, params:, version:, requirements:)
|
|
20
|
+
built_path = Path.new(path, namespace, settings)
|
|
21
|
+
new(origin: built_path.origin, suffix: built_path.suffix, anchor:, params:, version:, requirements:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(origin:, suffix:, anchor:, params:, version:, requirements:)
|
|
25
|
+
@origin = origin
|
|
26
|
+
@anchor = anchor
|
|
27
|
+
@version = version
|
|
28
|
+
@requirements = requirements
|
|
29
|
+
@path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
|
|
30
|
+
@pattern = MustermannPattern.new(@path, uri_decode: true, params:, capture: extract_capture(version, requirements))
|
|
24
31
|
@to_regexp = @pattern.to_regexp
|
|
32
|
+
@captures = @to_regexp.names.any?
|
|
25
33
|
end
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
# True when the compiled pattern has named captures to extract from a
|
|
36
|
+
# matched path. A fully static path has none — not even +format+, which
|
|
37
|
+
# the suffix spells as a literal — so asking Mustermann for its params
|
|
38
|
+
# would run the regexp a second time (the router's union already matched
|
|
39
|
+
# it) only to hand back an empty Hash.
|
|
40
|
+
#
|
|
41
|
+
# Resolved once here rather than per request: +Regexp#names+ builds an
|
|
42
|
+
# Array and a String per capture, and Ruby offers no predicate that skips
|
|
43
|
+
# that (+named_captures+ builds a Hash, and scanning the source for
|
|
44
|
+
# <tt>(?<</tt> would count lookbehinds, which name nothing).
|
|
45
|
+
def captures?
|
|
46
|
+
@captures
|
|
47
|
+
end
|
|
28
48
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
options
|
|
49
|
+
def captures_default
|
|
50
|
+
to_regexp.names
|
|
51
|
+
.delete_if { |n| DEFAULT_CAPTURES.include?(n) }
|
|
52
|
+
.to_h { |k| [k, ''] }
|
|
34
53
|
end
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
unless anchor || pattern.end_with?('*path')
|
|
38
|
-
pattern = +pattern
|
|
39
|
-
pattern << '/' unless pattern.end_with?('/')
|
|
40
|
-
pattern << '*path'
|
|
41
|
-
end
|
|
55
|
+
private
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
# The declared versions constrain the +:version+ capture. They are handed
|
|
58
|
+
# to Mustermann as one alternation Regexp rather than as the Array of
|
|
59
|
+
# Strings they arrive in, because an Array capture makes Mustermann build
|
|
60
|
+
# a *converter* for the capture -- and for an Array of plain Strings that
|
|
61
|
+
# converter is the identity function (Mustermann only derives one from a
|
|
62
|
+
# Class or a Symbol, so every entry contributes nothing and the lambda
|
|
63
|
+
# falls through to `|| string`).
|
|
64
|
+
#
|
|
65
|
+
# A non-empty converter table costs every request on the route: it makes
|
|
66
|
+
# Mustermann's +identity_params?+ fast path unreachable, so +params+
|
|
67
|
+
# rebuilds the capture Hash through +map_param+ and calls the do-nothing
|
|
68
|
+
# lambda on each value. Passing a Regexp registers no converter at all.
|
|
69
|
+
#
|
|
70
|
+
# The generated matcher differs in one respect: Mustermann expands an
|
|
71
|
+
# Array entry the way it expands a path literal, so each character also
|
|
72
|
+
# matches its own percent-encoding (+v1+ as <tt>(?:v|%76)(?:1|%31)</tt>).
|
|
73
|
+
# A Regexp is inserted verbatim, so a percent-encoded version segment no
|
|
74
|
+
# longer matches the route. It never reached the endpoint anyway --
|
|
75
|
+
# {Versioner::Base#potential_version_match?} compares the raw segment
|
|
76
|
+
# against the declared versions, so +/api/%76%31/x+ was matched here and
|
|
77
|
+
# then rejected as an unknown version, ending in the same cascading 404
|
|
78
|
+
# the router now returns directly.
|
|
79
|
+
#
|
|
80
|
+
# +Regexp.union+ takes Strings and Regexps only, and +version+ accepts
|
|
81
|
+
# Symbols and Integers too, so the entries are coerced first. A lone one
|
|
82
|
+
# would survive without it -- the single-argument path goes through
|
|
83
|
+
# +Regexp.escape+, which does accept a Symbol -- but a second raises
|
|
84
|
+
# TypeError.
|
|
85
|
+
def extract_capture(version, requirements)
|
|
86
|
+
return requirements if version.blank?
|
|
46
87
|
|
|
47
|
-
|
|
88
|
+
requirements.merge(version: Regexp.union(Array.wrap(version).map(&:to_s)))
|
|
48
89
|
end
|
|
49
90
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
def build_path_from_pattern(pattern, anchor)
|
|
92
|
+
return pattern.dup.insert(pattern.rindex('/') + 1, '?') if pattern.end_with?('*path')
|
|
93
|
+
return pattern if anchor
|
|
94
|
+
return "#{pattern}?*path" if pattern.end_with?('/')
|
|
95
|
+
|
|
96
|
+
"#{pattern}/?*path"
|
|
56
97
|
end
|
|
57
98
|
|
|
58
99
|
class PatternCache < Grape::Util::Cache
|
|
59
100
|
def initialize
|
|
101
|
+
super
|
|
60
102
|
@cache = Hash.new do |h, (pattern, suffix)|
|
|
61
103
|
h[[pattern, suffix]] = -"#{pattern}#{suffix}"
|
|
62
104
|
end
|
data/lib/grape/router/route.rb
CHANGED
|
@@ -1,90 +1,109 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'grape/router/pattern'
|
|
4
|
-
require 'grape/router/attribute_translator'
|
|
5
|
-
require 'forwardable'
|
|
6
|
-
require 'pathname'
|
|
7
|
-
|
|
8
3
|
module Grape
|
|
9
4
|
class Router
|
|
10
|
-
class Route
|
|
11
|
-
|
|
12
|
-
SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze
|
|
13
|
-
FIXED_NAMED_CAPTURES = %w[format version].freeze
|
|
5
|
+
class Route < BaseRoute
|
|
6
|
+
extend Forwardable
|
|
14
7
|
|
|
15
|
-
|
|
8
|
+
FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
|
|
9
|
+
NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
attr_reader :app, :request_method, :index
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
def_delegators :pattern, :path, :origin
|
|
21
|
-
delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
|
|
22
|
-
|
|
23
|
-
def method_missing(method_id, *arguments)
|
|
24
|
-
match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
|
|
25
|
-
if match
|
|
26
|
-
method_name = match.captures.last.to_sym
|
|
27
|
-
warn_route_methods(method_name, caller(1).shift)
|
|
28
|
-
@options[method_name]
|
|
29
|
-
else
|
|
30
|
-
super
|
|
31
|
-
end
|
|
32
|
-
end
|
|
13
|
+
def_delegators :@app, :call
|
|
33
14
|
|
|
34
|
-
def
|
|
35
|
-
|
|
15
|
+
def initialize(endpoint, method, pattern, options, forward_match:, params: {}, **route_attributes)
|
|
16
|
+
super(pattern, options, **route_attributes)
|
|
17
|
+
@app = endpoint
|
|
18
|
+
@request_method = upcase_method(method)
|
|
19
|
+
@match_function = forward_match ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
|
|
20
|
+
@declared_params = params
|
|
36
21
|
end
|
|
37
22
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
def to_head
|
|
24
|
+
head = dup
|
|
25
|
+
head.convert_to_head_request!
|
|
26
|
+
head
|
|
41
27
|
end
|
|
42
28
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
def apply(app)
|
|
30
|
+
@app = app
|
|
31
|
+
self
|
|
46
32
|
end
|
|
47
33
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase
|
|
34
|
+
def match?(input)
|
|
35
|
+
return false if input.blank?
|
|
51
36
|
|
|
52
|
-
@
|
|
53
|
-
@pattern = Pattern.new(pattern, **options)
|
|
54
|
-
@translator = AttributeTranslator.new(**options, request_method: method_upcase)
|
|
37
|
+
@match_function.call(input, pattern)
|
|
55
38
|
end
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
# The route's declared params keyed by name — path captures plus any
|
|
41
|
+
# declared body/query params, as their definitions. Used for documentation
|
|
42
|
+
# (e.g. grape-swagger), not for extracting request values.
|
|
43
|
+
def params
|
|
44
|
+
@params ||= pattern.captures_default.merge(@declared_params)
|
|
59
45
|
end
|
|
60
46
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
47
|
+
# Extract param values from a matched request path. Used by the router.
|
|
48
|
+
#
|
|
49
|
+
# A pattern with no named captures has nothing to extract, so it skips the
|
|
50
|
+
# match entirely and answers nil — the same way {GreedyRoute#params_for}
|
|
51
|
+
# does, and what the router already coerces into the Hash it builds
|
|
52
|
+
# routing args in.
|
|
53
|
+
def params_for(input)
|
|
54
|
+
return unless pattern.captures?
|
|
55
|
+
|
|
56
|
+
parsed = pattern.params(input)
|
|
57
|
+
return unless parsed
|
|
58
|
+
|
|
59
|
+
params = {}
|
|
60
|
+
parsed.each { |name, value| params[name.to_sym] = tag_utf8!(value) unless value.nil? }
|
|
61
|
+
params
|
|
64
62
|
end
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
protected
|
|
65
|
+
|
|
66
|
+
def convert_to_head_request!
|
|
67
|
+
@request_method = Rack::HEAD
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Mustermann decodes path captures out of +PATH_INFO+, which Rack hands us
|
|
73
|
+
# tagged ASCII-8BIT, so path params came back binary while Rack tags query
|
|
74
|
+
# and body params UTF-8. That split makes an API's own declarations
|
|
75
|
+
# disagree with themselves: `values: ['café']` matched `?id=café` but not
|
|
76
|
+
# `/café`, since a binary string never equals the UTF-8 literal it was
|
|
77
|
+
# written as.
|
|
78
|
+
#
|
|
79
|
+
# Re-tag as UTF-8. Nothing obliges a client to send UTF-8: the request
|
|
80
|
+
# target is octets to HTTP, and Rack's SPEC has CGI keys carry non-ASCII
|
|
81
|
+
# as ASCII-8BIT. But UTF-8 is what browsers percent-encode with, what an
|
|
82
|
+
# IRI maps to, and what Rails settles on — ActionDispatch::Journey::Router
|
|
83
|
+
# force_encodes every path capture to UTF-8 after unescaping it.
|
|
84
|
+
#
|
|
85
|
+
# Only the encoding changes; the bytes are untouched. Octets that are not
|
|
86
|
+
# UTF-8 therefore stay invalid and are caught downstream rather than being
|
|
87
|
+
# silently scrubbed into something the client never sent.
|
|
88
|
+
#
|
|
89
|
+
# The re-tag is in place, hence the bang. Mustermann's +Pattern#params+
|
|
90
|
+
# builds a fresh Hash of fresh, unfrozen strings on every call and skips
|
|
91
|
+
# its own Match cache, so the mutation cannot escape this request — which
|
|
92
|
+
# is also what lets the Array branch re-tag its elements with +each+.
|
|
93
|
+
def tag_utf8!(value)
|
|
94
|
+
case value
|
|
95
|
+
when String
|
|
96
|
+
value.force_encoding(Encoding::UTF_8)
|
|
97
|
+
when Array
|
|
98
|
+
value.each { |v| tag_utf8!(v) }
|
|
75
99
|
else
|
|
76
|
-
|
|
77
|
-
parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {}
|
|
100
|
+
value
|
|
78
101
|
end
|
|
79
102
|
end
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
|
|
85
|
-
path = File.realpath(path) if Pathname.new(path).relative?
|
|
86
|
-
expected ||= name
|
|
87
|
-
ActiveSupport::Deprecation.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.")
|
|
104
|
+
def upcase_method(method)
|
|
105
|
+
method_s = method.to_s
|
|
106
|
+
Grape::HTTP_SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
|
|
88
107
|
end
|
|
89
108
|
end
|
|
90
109
|
end
|
data/lib/grape/router.rb
CHANGED
|
@@ -1,60 +1,54 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'grape/router/route'
|
|
4
|
-
require 'grape/util/cache'
|
|
5
|
-
|
|
6
3
|
module Grape
|
|
7
4
|
class Router
|
|
8
|
-
attr_reader :map, :compiled
|
|
9
|
-
|
|
10
|
-
def self.normalize_path(path)
|
|
11
|
-
path = +"/#{path}"
|
|
12
|
-
path.squeeze!('/')
|
|
13
|
-
path.sub!(%r{/+\Z}, '')
|
|
14
|
-
path = '/' if path == ''
|
|
15
|
-
path
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.supported_methods
|
|
19
|
-
@supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
|
|
20
|
-
end
|
|
21
|
-
|
|
22
5
|
def initialize
|
|
23
6
|
@neutral_map = []
|
|
24
7
|
@neutral_regexes = []
|
|
25
|
-
|
|
26
|
-
|
|
8
|
+
# Plain hashes with no auto-vivifying default: a lookup for an HTTP method
|
|
9
|
+
# that has no routes must not insert a key. `compile!` freezes both maps,
|
|
10
|
+
# so request-time reads never mutate shared state (see #match? / #rotation).
|
|
11
|
+
@map = {}
|
|
12
|
+
@optimized_map = {}
|
|
27
13
|
end
|
|
28
14
|
|
|
29
15
|
def compile!
|
|
30
|
-
return if compiled
|
|
16
|
+
return if @compiled
|
|
31
17
|
|
|
32
|
-
@union = Regexp.union(@neutral_regexes)
|
|
18
|
+
@union = resolve_capture_groups(Regexp.union(@neutral_regexes), @neutral_map)
|
|
33
19
|
@neutral_regexes = nil
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
20
|
+
# Compiled from the routes actually registered rather than from
|
|
21
|
+
# Grape::HTTP_SUPPORTED_METHODS. A route declared with any other verb
|
|
22
|
+
# (`route :purge, '/cache'`) is accepted at definition time and is
|
|
23
|
+
# already collected into the resource's Allow header, so skipping it here
|
|
24
|
+
# left it in @map but out of @optimized_map — the only map #match? reads.
|
|
25
|
+
# The resource then advertised a method that could never be matched and
|
|
26
|
+
# answered every request for it with 405.
|
|
27
|
+
@map.each do |method, routes|
|
|
28
|
+
optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
|
|
29
|
+
@optimized_map[method] = resolve_capture_groups(Regexp.union(optimized_map), routes)
|
|
41
30
|
end
|
|
31
|
+
@map.freeze
|
|
32
|
+
@optimized_map.freeze
|
|
42
33
|
@compiled = true
|
|
43
34
|
end
|
|
44
35
|
|
|
45
36
|
def append(route)
|
|
46
|
-
map[route.request_method] << route
|
|
37
|
+
(@map[route.request_method] ||= []) << route
|
|
47
38
|
end
|
|
48
39
|
|
|
49
|
-
def associate_routes(
|
|
50
|
-
@neutral_regexes <<
|
|
51
|
-
@neutral_map <<
|
|
40
|
+
def associate_routes(greedy_route)
|
|
41
|
+
@neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
|
|
42
|
+
@neutral_map << greedy_route
|
|
52
43
|
end
|
|
53
44
|
|
|
54
45
|
def call(env)
|
|
55
46
|
with_optimization do
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
# Published on the env so the middleware the matched route runs -- the
|
|
48
|
+
# path versioner above all -- reads the path this routed on instead of
|
|
49
|
+
# normalizing PATH_INFO a second time.
|
|
50
|
+
input = env[Grape::Env::GRAPE_NORMALIZED_PATH] = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
|
|
51
|
+
transaction(input, env[Rack::REQUEST_METHOD], env)
|
|
58
52
|
end
|
|
59
53
|
end
|
|
60
54
|
|
|
@@ -65,113 +59,143 @@ module Grape
|
|
|
65
59
|
any.endpoint
|
|
66
60
|
end
|
|
67
61
|
|
|
62
|
+
DEFAULT_RESPONSE_HEADERS = Grape::Util::Header.new.merge('X-Cascade' => 'pass').freeze
|
|
63
|
+
DEFAULT_RESPONSE_BODY = ['404 Not Found'].freeze
|
|
64
|
+
|
|
68
65
|
private
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
# Resolve +input+ against the compiled routes, in priority order:
|
|
68
|
+
#
|
|
69
|
+
# 1. the routes registered for +method+ — the compiled-union match first,
|
|
70
|
+
# then, when that route cascades, its siblings (see #rotation);
|
|
71
|
+
# 2. the ANY (+'*'+) routes;
|
|
72
|
+
# 3. the greedy neighbour, which answers auto-OPTIONS and 405.
|
|
73
|
+
#
|
|
74
|
+
# Returns nil when nothing answered, leaving the caller to 404. A response
|
|
75
|
+
# that cascades is never final: it is returned only once every later
|
|
76
|
+
# candidate has declined too, so the caller (or a mounting app upstream)
|
|
77
|
+
# can keep looking.
|
|
78
|
+
def transaction(input, method, env)
|
|
79
|
+
exact_route = match?(input, method)
|
|
80
|
+
response = process_route(exact_route, input, env) if exact_route
|
|
81
|
+
return response if halt?(response)
|
|
82
|
+
|
|
83
|
+
# A cascading route has only declined this request. Its siblings — the
|
|
84
|
+
# routes sharing this path but differing in, say, version — must be
|
|
85
|
+
# tried before falling back to the ANY routes and the greedy neighbour.
|
|
86
|
+
# Skipped when nothing matched: the compiled union is the disjunction of
|
|
87
|
+
# the same patterns #rotation walks, so a miss there is a miss here.
|
|
88
|
+
cascaded = !response.nil?
|
|
89
|
+
if cascaded
|
|
90
|
+
response = rotation(input, method, env, exact_route)
|
|
91
|
+
return response if response && !cascade?(response)
|
|
75
92
|
end
|
|
76
|
-
[response, route]
|
|
77
|
-
end
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
response = nil
|
|
81
|
-
input, method = *extract_input_and_method(env)
|
|
82
|
-
map[method].each do |route|
|
|
83
|
-
next if exact_route == route
|
|
84
|
-
next unless route.match?(input)
|
|
94
|
+
last_neighbor_route = greedy_match?(input)
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
response
|
|
90
|
-
end
|
|
96
|
+
# If last_neighbor_route exists and request method is OPTIONS,
|
|
97
|
+
# return response by using #include_allow_header.
|
|
98
|
+
return process_route(last_neighbor_route, input, env, include_allow_header: true) if !cascaded && method == Rack::OPTIONS && last_neighbor_route
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
input, method = *extract_input_and_method(env)
|
|
94
|
-
response = yield(input, method)
|
|
100
|
+
star_route = match?(input, '*')
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
if star_route
|
|
103
|
+
close_body(response) if response # superseded by the ANY route
|
|
104
|
+
response = process_route(star_route, input, env)
|
|
105
|
+
return response if halt?(response)
|
|
97
106
|
|
|
98
|
-
|
|
107
|
+
cascaded ||= !response.nil?
|
|
108
|
+
end
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
# return response by using #call_with_allow_headers.
|
|
102
|
-
return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade
|
|
110
|
+
return process_route(last_neighbor_route, input, env, include_allow_header: true) if !cascaded && last_neighbor_route
|
|
103
111
|
|
|
104
|
-
|
|
112
|
+
response
|
|
113
|
+
end
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
# The routes registered for +method+ other than +exact_route+, tried in
|
|
116
|
+
# registration order until one answers without cascading. Returns the last
|
|
117
|
+
# response processed — a cascading one when every sibling declined, so the
|
|
118
|
+
# caller can hand it back — or nil when no sibling matched.
|
|
119
|
+
def rotation(input, method, env, exact_route)
|
|
120
|
+
response = nil
|
|
121
|
+
@map[method]&.each do |route|
|
|
122
|
+
next if exact_route == route
|
|
123
|
+
next unless route.match?(input)
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
response = process_route(route, env)
|
|
110
|
-
|
|
125
|
+
close_body(response) if response # the previous sibling cascaded
|
|
126
|
+
response = process_route(route, input, env)
|
|
127
|
+
break unless cascade?(response)
|
|
111
128
|
end
|
|
129
|
+
response
|
|
130
|
+
end
|
|
112
131
|
|
|
113
|
-
|
|
132
|
+
# Returns true if `response` should be returned as-is from the enclosing
|
|
133
|
+
# transaction. Closes the body as a side effect when the response is
|
|
134
|
+
# cascading so callers can safely try the next match.
|
|
135
|
+
def halt?(response)
|
|
136
|
+
return false unless response
|
|
114
137
|
|
|
115
|
-
|
|
138
|
+
cascade = cascade?(response)
|
|
139
|
+
close_body(response) if cascade
|
|
140
|
+
!cascade
|
|
116
141
|
end
|
|
117
142
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
# Releases a response the router has decided not to return. Rack requires
|
|
144
|
+
# every body it hands out to be closed, and a cascading candidate is
|
|
145
|
+
# discarded as soon as a later one answers.
|
|
146
|
+
def close_body(response)
|
|
147
|
+
body = response[2]
|
|
148
|
+
body.close if body.respond_to?(:close)
|
|
121
149
|
end
|
|
122
150
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
# Routing args are rebuilt for every attempt: when a route cascades
|
|
152
|
+
# (X-Cascade pass), the next candidate must not observe the previous
|
|
153
|
+
# attempt's +route_info+ or path captures.
|
|
154
|
+
def process_route(route, input, env, include_allow_header: false)
|
|
155
|
+
# The path captures are the hash: +route_info+ is written into them
|
|
156
|
+
# rather than merged in from a second one.
|
|
157
|
+
routing_args = route.params_for(input) || {}
|
|
158
|
+
routing_args[:route_info] = route
|
|
159
|
+
env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args
|
|
160
|
+
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
|
|
161
|
+
route.call(env)
|
|
126
162
|
end
|
|
127
163
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
164
|
+
# Tells each route the number of the group it ended up as in +union+. The
|
|
165
|
+
# numbering is a property of the union rather than of the route's own
|
|
166
|
+
# pattern -- every route ahead of it contributes however many groups its
|
|
167
|
+
# pattern declares -- so it can only be resolved once the union is built.
|
|
168
|
+
# Returns the union, so a caller can assign it in one expression.
|
|
169
|
+
def resolve_capture_groups(union, routes)
|
|
170
|
+
named_captures = union.named_captures
|
|
171
|
+
routes.each { |route| route.resolve_capture_group!(named_captures) }
|
|
172
|
+
union
|
|
132
173
|
end
|
|
133
174
|
|
|
134
175
|
def with_optimization
|
|
135
|
-
compile!
|
|
176
|
+
compile!
|
|
136
177
|
yield || default_response
|
|
137
178
|
end
|
|
138
179
|
|
|
139
180
|
def default_response
|
|
140
|
-
[404,
|
|
181
|
+
[404, DEFAULT_RESPONSE_HEADERS.dup, DEFAULT_RESPONSE_BODY.dup]
|
|
141
182
|
end
|
|
142
183
|
|
|
184
|
+
# Which alternative of the union matched is answered by scanning one group
|
|
185
|
+
# per registered route, so on an API with many of them that scan is what a
|
|
186
|
+
# request costs. The groups are indexed by number rather than by name: a
|
|
187
|
+
# name sends MatchData through the pattern's name table on every lookup,
|
|
188
|
+
# a number indexes the match region directly.
|
|
143
189
|
def match?(input, method)
|
|
144
|
-
|
|
145
|
-
return unless current_regexp.match(input)
|
|
146
|
-
|
|
147
|
-
last_match = Regexp.last_match
|
|
148
|
-
@map[method].detect { |route| last_match["_#{route.index}"] }
|
|
190
|
+
@optimized_map[method]&.match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_group] } }
|
|
149
191
|
end
|
|
150
192
|
|
|
151
193
|
def greedy_match?(input)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
last_match = Regexp.last_match
|
|
155
|
-
@neutral_map.detect { |route| last_match["_#{route.index}"] }
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def call_with_allow_headers(env, route)
|
|
159
|
-
prepare_env_from_route(env, route)
|
|
160
|
-
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
|
|
161
|
-
route.endpoint.call(env)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def prepare_env_from_route(env, route)
|
|
165
|
-
input, = *extract_input_and_method(env)
|
|
166
|
-
env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
|
|
194
|
+
@union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_group] } }
|
|
167
195
|
end
|
|
168
196
|
|
|
169
197
|
def cascade?(response)
|
|
170
|
-
response && response[1][
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def string_for(input)
|
|
174
|
-
self.class.normalize_path(input)
|
|
198
|
+
response && response[1]['X-Cascade'] == 'pass'
|
|
175
199
|
end
|
|
176
200
|
end
|
|
177
201
|
end
|
|
@@ -31,6 +31,13 @@ module Grape
|
|
|
31
31
|
def ==(other)
|
|
32
32
|
path == other.path
|
|
33
33
|
end
|
|
34
|
+
alias eql? ==
|
|
35
|
+
|
|
36
|
+
# Mirrors #==, which keys on the path alone and does not check the
|
|
37
|
+
# class, so the class must stay out of the hash too.
|
|
38
|
+
def hash
|
|
39
|
+
path.hash
|
|
40
|
+
end
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
43
|
end
|
|
@@ -6,11 +6,9 @@ module Grape
|
|
|
6
6
|
# for using Rack::SendFile middleware
|
|
7
7
|
class SendfileResponse < Rack::Response
|
|
8
8
|
def respond_to?(method_name, include_all = false)
|
|
9
|
-
if method_name == :to_path
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
super
|
|
13
|
-
end
|
|
9
|
+
return @body.respond_to?(:to_path, include_all) if method_name == :to_path
|
|
10
|
+
|
|
11
|
+
super
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def to_path
|
|
@@ -18,6 +18,13 @@ module Grape
|
|
|
18
18
|
def ==(other)
|
|
19
19
|
stream == other.stream
|
|
20
20
|
end
|
|
21
|
+
alias eql? ==
|
|
22
|
+
|
|
23
|
+
# Mirrors #==, which keys on the stream alone and does not check the
|
|
24
|
+
# class, so the class must stay out of the hash too.
|
|
25
|
+
def hash
|
|
26
|
+
stream.hash
|
|
27
|
+
end
|
|
21
28
|
end
|
|
22
29
|
end
|
|
23
30
|
end
|