grape 2.4.0 → 3.2.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +109 -0
  3. data/CONTRIBUTING.md +2 -10
  4. data/README.md +137 -181
  5. data/UPGRADING.md +154 -0
  6. data/grape.gemspec +4 -4
  7. data/lib/grape/api/instance.rb +51 -104
  8. data/lib/grape/api.rb +29 -42
  9. data/lib/grape/content_types.rb +1 -4
  10. data/lib/grape/declared_params_handler.rb +118 -0
  11. data/lib/grape/dry_types.rb +48 -4
  12. data/lib/grape/dsl/callbacks.rb +8 -58
  13. data/lib/grape/dsl/declared.rb +35 -0
  14. data/lib/grape/dsl/desc.rb +8 -67
  15. data/lib/grape/dsl/helpers.rb +59 -64
  16. data/lib/grape/dsl/inside_route.rb +28 -189
  17. data/lib/grape/dsl/logger.rb +3 -6
  18. data/lib/grape/dsl/middleware.rb +22 -40
  19. data/lib/grape/dsl/parameters.rb +24 -51
  20. data/lib/grape/dsl/request_response.rb +136 -139
  21. data/lib/grape/dsl/routing.rb +240 -200
  22. data/lib/grape/dsl/settings.rb +23 -135
  23. data/lib/grape/dsl/validations.rb +38 -44
  24. data/lib/grape/endpoint.rb +169 -205
  25. data/lib/grape/error_formatter/base.rb +4 -2
  26. data/lib/grape/exceptions/base.rb +19 -45
  27. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  28. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  29. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  30. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  31. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  32. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  33. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  34. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  35. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  36. data/lib/grape/exceptions/request_error.rb +11 -0
  37. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  38. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  39. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  40. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  41. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  42. data/lib/grape/exceptions/validation.rb +7 -4
  43. data/lib/grape/exceptions/validation_errors.rb +13 -7
  44. data/lib/grape/locale/en.yml +0 -5
  45. data/lib/grape/middleware/auth/base.rb +2 -0
  46. data/lib/grape/middleware/auth/dsl.rb +9 -10
  47. data/lib/grape/middleware/base.rb +6 -4
  48. data/lib/grape/middleware/error.rb +4 -14
  49. data/lib/grape/middleware/formatter.rb +11 -7
  50. data/lib/grape/middleware/stack.rb +3 -2
  51. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  52. data/lib/grape/middleware/versioner/base.rb +42 -40
  53. data/lib/grape/middleware/versioner/header.rb +2 -18
  54. data/lib/grape/middleware/versioner/param.rb +2 -2
  55. data/lib/grape/middleware/versioner/path.rb +2 -2
  56. data/lib/grape/namespace.rb +15 -8
  57. data/lib/grape/params_builder/base.rb +2 -0
  58. data/lib/grape/params_builder.rb +2 -19
  59. data/lib/grape/request.rb +2 -10
  60. data/lib/grape/router/base_route.rb +14 -5
  61. data/lib/grape/router/greedy_route.rb +11 -5
  62. data/lib/grape/router/pattern.rb +6 -20
  63. data/lib/grape/router/route.rb +7 -11
  64. data/lib/grape/router.rb +42 -65
  65. data/lib/grape/util/api_description.rb +58 -0
  66. data/lib/grape/util/base_inheritable.rb +5 -2
  67. data/lib/grape/util/cache.rb +1 -0
  68. data/lib/grape/util/deep_freeze.rb +35 -0
  69. data/lib/grape/util/inheritable_setting.rb +8 -1
  70. data/lib/grape/util/media_type.rb +2 -2
  71. data/lib/grape/util/registry.rb +1 -1
  72. data/lib/grape/util/translation.rb +42 -0
  73. data/lib/grape/validations/attributes_iterator.rb +35 -20
  74. data/lib/grape/validations/contract_scope.rb +2 -8
  75. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  76. data/lib/grape/validations/param_scope_tracker.rb +57 -0
  77. data/lib/grape/validations/params_documentation.rb +50 -0
  78. data/lib/grape/validations/params_scope.rb +147 -156
  79. data/lib/grape/validations/single_attribute_iterator.rb +2 -2
  80. data/lib/grape/validations/types/array_coercer.rb +2 -3
  81. data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
  82. data/lib/grape/validations/types/primitive_coercer.rb +1 -28
  83. data/lib/grape/validations/types.rb +10 -25
  84. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  85. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  86. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  87. data/lib/grape/validations/validators/base.rb +95 -25
  88. data/lib/grape/validations/validators/coerce_validator.rb +15 -35
  89. data/lib/grape/validations/validators/contract_scope_validator.rb +9 -5
  90. data/lib/grape/validations/validators/default_validator.rb +12 -18
  91. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  92. data/lib/grape/validations/validators/except_values_validator.rb +13 -4
  93. data/lib/grape/validations/validators/length_validator.rb +21 -22
  94. data/lib/grape/validations/validators/multiple_params_base.rb +5 -5
  95. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  96. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  97. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  98. data/lib/grape/validations/validators/same_as_validator.rb +6 -15
  99. data/lib/grape/validations/validators/values_validator.rb +29 -21
  100. data/lib/grape/version.rb +1 -1
  101. data/lib/grape.rb +29 -23
  102. metadata +31 -26
  103. data/lib/grape/api/helpers.rb +0 -9
  104. data/lib/grape/dsl/api.rb +0 -17
  105. data/lib/grape/dsl/configuration.rb +0 -15
  106. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  107. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  108. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  109. data/lib/grape/exceptions/missing_option.rb +0 -11
  110. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  111. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  112. data/lib/grape/exceptions/unknown_options.rb +0 -11
  113. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  114. data/lib/grape/extensions/hash.rb +0 -27
  115. data/lib/grape/extensions/hashie/mash.rb +0 -24
  116. data/lib/grape/types/invalid_value.rb +0 -8
  117. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  118. data/lib/grape/validations/attributes_doc.rb +0 -60
  119. data/lib/grape/validations/validator_factory.rb +0 -15
@@ -5,68 +5,70 @@ module Grape
5
5
  module Versioner
6
6
  class Base < Grape::Middleware::Base
7
7
  DEFAULT_OPTIONS = {
8
- pattern: /.*/i.freeze,
8
+ pattern: /.*/i,
9
+ prefix: nil,
10
+ mount_path: nil,
9
11
  version_options: {
10
12
  strict: false,
11
13
  cascade: true,
12
- parameter: 'apiver'
14
+ parameter: 'apiver',
15
+ vendor: nil
13
16
  }.freeze
14
17
  }.freeze
15
18
 
16
- def self.inherited(klass)
17
- super
18
- Versioner.register(klass)
19
- end
20
-
21
- def versions
22
- options[:versions]
23
- end
24
-
25
- def prefix
26
- options[:prefix]
27
- end
19
+ CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze
28
20
 
29
- def mount_path
30
- options[:mount_path]
21
+ DEFAULT_OPTIONS.each_key do |key|
22
+ define_method key do
23
+ options[key]
24
+ end
31
25
  end
32
26
 
33
- def pattern
34
- options[:pattern]
27
+ DEFAULT_OPTIONS[:version_options].each_key do |key|
28
+ define_method key do
29
+ options[:version_options][key]
30
+ end
35
31
  end
36
32
 
37
- def version_options
38
- options[:version_options]
33
+ def self.inherited(klass)
34
+ super
35
+ Versioner.register(klass)
39
36
  end
40
37
 
41
- def strict?
42
- version_options[:strict]
43
- end
38
+ attr_reader :error_headers, :versions
44
39
 
45
- # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
46
- # of routes (see Grape::Router) for more information). To prevent
47
- # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
48
- def cascade?
49
- version_options[:cascade]
40
+ def initialize(app, **options)
41
+ super
42
+ @error_headers = cascade ? CASCADE_PASS_HEADER : {}
43
+ @versions = options[:versions]&.map(&:to_s) # making sure versions are strings to ease potential match
50
44
  end
51
45
 
52
- def parameter_key
53
- version_options[:parameter]
46
+ def potential_version_match?(potential_version)
47
+ versions.blank? || versions.include?(potential_version)
54
48
  end
55
49
 
56
- def vendor
57
- version_options[:vendor]
50
+ def version_not_found!
51
+ throw :error, status: 404, message: '404 API Version Not Found', headers: CASCADE_PASS_HEADER
58
52
  end
59
53
 
60
- def error_headers
61
- cascade? ? { 'X-Cascade' => 'pass' } : {}
62
- end
54
+ private
63
55
 
64
- def potential_version_match?(potential_version)
65
- versions.blank? || versions.any? { |v| v.to_s == potential_version }
66
- end
56
+ def available_media_types
57
+ @available_media_types ||= begin
58
+ media_types = []
59
+ base_media_type = "application/vnd.#{vendor}"
60
+ content_types.each_key do |extension|
61
+ versions&.reverse_each do |version|
62
+ media_types << "#{base_media_type}-#{version}+#{extension}"
63
+ media_types << "#{base_media_type}-#{version}"
64
+ end
65
+ media_types << "#{base_media_type}+#{extension}"
66
+ end
67
67
 
68
- def version_not_found!
69
- throw :error, status: 404, message: '404 API Version Not Found', headers: { 'X-Cascade' => 'pass' }
68
+ media_types << base_media_type
69
+ media_types.concat(content_types.values.flatten)
70
+ media_types
71
+ end
70
72
  end
71
73
  end
72
74
  end
@@ -53,7 +53,7 @@ module Grape
53
53
  end
54
54
 
55
55
  def strict_header_checks!
56
- return unless strict?
56
+ return unless strict
57
57
 
58
58
  accept_header_check!
59
59
  version_and_vendor_check!
@@ -101,26 +101,10 @@ module Grape
101
101
  end
102
102
 
103
103
  def version_not_found!(media_types)
104
- return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) }
104
+ return unless media_types.all? { |media_type| media_type&.version && versions && !versions.include?(media_type.version) }
105
105
 
106
106
  invalid_version_header!('API version not found.')
107
107
  end
108
-
109
- def available_media_types
110
- [].tap do |available_media_types|
111
- base_media_type = "application/vnd.#{vendor}"
112
- content_types.each_key do |extension|
113
- versions&.reverse_each do |version|
114
- available_media_types << "#{base_media_type}-#{version}+#{extension}"
115
- available_media_types << "#{base_media_type}-#{version}"
116
- end
117
- available_media_types << "#{base_media_type}+#{extension}"
118
- end
119
-
120
- available_media_types << base_media_type
121
- available_media_types.concat(content_types.values.flatten)
122
- end
123
- end
124
108
  end
125
109
  end
126
110
  end
@@ -20,11 +20,11 @@ module Grape
20
20
  # env['api.version'] => 'v1'
21
21
  class Param < Base
22
22
  def before
23
- potential_version = query_params[parameter_key]
23
+ potential_version = query_params[parameter]
24
24
  return if potential_version.blank?
25
25
 
26
26
  version_not_found! unless potential_version_match?(potential_version)
27
- env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key)
27
+ env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter)
28
28
  end
29
29
  end
30
30
  end
@@ -22,13 +22,13 @@ module Grape
22
22
  return if path_info == '/'
23
23
 
24
24
  [mount_path, Grape::Router.normalize_path(prefix)].each do |path|
25
- path_info.delete_prefix!(path) if path.present? && path != '/' && path_info.start_with?(path)
25
+ path_info = path_info.delete_prefix(path) if path.present? && path != '/' && path_info.start_with?(path)
26
26
  end
27
27
 
28
28
  slash_position = path_info.index('/', 1) # omit the first one
29
29
  return unless slash_position
30
30
 
31
- potential_version = path_info[1..slash_position - 1]
31
+ potential_version = path_info[1..(slash_position - 1)]
32
32
  return unless potential_version.match?(pattern)
33
33
 
34
34
  version_not_found! unless potential_version_match?(potential_version)
@@ -5,29 +5,36 @@ module Grape
5
5
  # logical grouping of endpoints as well as sharing common configuration.
6
6
  # May also be referred to as group, segment, or resource.
7
7
  class Namespace
8
- attr_reader :space, :options
8
+ attr_reader :space, :requirements, :options
9
9
 
10
10
  # @param space [String] the name of this namespace
11
11
  # @param options [Hash] options hash
12
12
  # @option options :requirements [Hash] param-regex pairs, all of which must
13
13
  # be met by a request's params for all endpoints in this namespace, or
14
14
  # validation will fail and return a 422.
15
- def initialize(space, options)
15
+ def initialize(space, requirements: nil, **options)
16
16
  @space = space.to_s
17
+ @requirements = requirements
17
18
  @options = options
18
19
  end
19
20
 
20
- # Retrieves the requirements from the options hash, if given.
21
- # @return [Hash]
22
- def requirements
23
- options[:requirements] || {}
24
- end
25
-
26
21
  # (see ::joined_space_path)
27
22
  def self.joined_space(settings)
28
23
  settings&.map(&:space)
29
24
  end
30
25
 
26
+ def eql?(other)
27
+ other.class == self.class &&
28
+ other.space == space &&
29
+ other.requirements == requirements &&
30
+ other.options == options
31
+ end
32
+ alias == eql?
33
+
34
+ def hash
35
+ [self.class, space, requirements, options].hash
36
+ end
37
+
31
38
  # Join the namespaces from a list of settings to create a path prefix.
32
39
  # @param settings [Array] list of Grape::Util::InheritableSettings.
33
40
  def self.joined_space_path(settings)
@@ -8,6 +8,8 @@ module Grape
8
8
  raise NotImplementedError
9
9
  end
10
10
 
11
+ private
12
+
11
13
  def inherited(klass)
12
14
  super
13
15
  ParamsBuilder.register(klass)
@@ -4,29 +4,12 @@ module Grape
4
4
  module ParamsBuilder
5
5
  extend Grape::Util::Registry
6
6
 
7
- SHORT_NAME_LOOKUP = {
8
- 'Grape::Extensions::Hash::ParamBuilder' => :hash,
9
- 'Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder' => :hash_with_indifferent_access,
10
- 'Grape::Extensions::Hashie::Mash::ParamBuilder' => :hashie_mash
11
- }.freeze
12
-
13
7
  module_function
14
8
 
15
9
  def params_builder_for(short_name)
16
- verified_short_name = verify_short_name!(short_name)
17
-
18
- raise Grape::Exceptions::UnknownParamsBuilder, verified_short_name unless registry.key?(verified_short_name)
19
-
20
- registry[verified_short_name]
21
- end
22
-
23
- def verify_short_name!(short_name)
24
- return short_name if short_name.is_a?(Symbol)
10
+ raise Grape::Exceptions::UnknownParamsBuilder, short_name unless registry.key?(short_name)
25
11
 
26
- class_name = short_name.name
27
- SHORT_NAME_LOOKUP[class_name].tap do |real_short_name|
28
- Grape.deprecator.warn "#{class_name} has been deprecated. Use short name :#{real_short_name} instead."
29
- end
12
+ registry[short_name]
30
13
  end
31
14
  end
32
15
  end
data/lib/grape/request.rb CHANGED
@@ -166,16 +166,8 @@ module Grape
166
166
 
167
167
  def make_params
168
168
  @params_builder.call(rack_params).deep_merge!(grape_routing_args)
169
- rescue EOFError
170
- raise Grape::Exceptions::EmptyMessageBody.new(content_type)
171
- rescue Rack::Multipart::MultipartPartLimitError, Rack::Multipart::MultipartTotalPartLimitError
172
- raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
173
- rescue Rack::QueryParser::ParamsTooDeepError
174
- raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
175
- rescue Rack::Utils::ParameterTypeError
176
- raise Grape::Exceptions::ConflictingTypes
177
- rescue Rack::Utils::InvalidParameterError
178
- raise Grape::Exceptions::InvalidParameters
169
+ rescue *Grape::RACK_ERRORS
170
+ raise Grape::Exceptions::RequestError
179
171
  end
180
172
 
181
173
  def build_headers
@@ -3,22 +3,31 @@
3
3
  module Grape
4
4
  class Router
5
5
  class BaseRoute
6
+ extend Forwardable
7
+
6
8
  delegate_missing_to :@options
7
9
 
8
- attr_reader :index, :pattern, :options
10
+ attr_reader :options, :pattern
11
+
12
+ def_delegators :@pattern, :path, :origin
13
+ def_delegators :@options, :description, :version, :requirements, :prefix, :anchor, :settings, :forward_match, *Grape::Util::ApiDescription::DSL_METHODS
9
14
 
10
- def initialize(options)
15
+ def initialize(pattern, options = {})
16
+ @pattern = pattern
11
17
  @options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
12
18
  end
13
19
 
14
- alias attributes options
20
+ # see https://github.com/ruby-grape/grape/issues/1348
21
+ def namespace
22
+ @namespace ||= @options[:namespace]
23
+ end
15
24
 
16
25
  def regexp_capture_index
17
- CaptureIndexCache[index]
26
+ @regexp_capture_index ||= CaptureIndexCache[@index]
18
27
  end
19
28
 
20
29
  def pattern_regexp
21
- pattern.to_regexp
30
+ @pattern.to_regexp
22
31
  end
23
32
 
24
33
  def to_regexp(index)
@@ -6,14 +6,20 @@
6
6
  module Grape
7
7
  class Router
8
8
  class GreedyRoute < BaseRoute
9
- def initialize(pattern, options)
10
- @pattern = pattern
11
- super(options)
9
+ extend Forwardable
10
+
11
+ def_delegators :@endpoint, :call
12
+
13
+ attr_reader :endpoint, :allow_header
14
+
15
+ def initialize(pattern, endpoint:, allow_header:)
16
+ super(pattern)
17
+ @endpoint = endpoint
18
+ @allow_header = allow_header
12
19
  end
13
20
 
14
- # Grape::Router:Route defines params as a function
15
21
  def params(_input = nil)
16
- options[:params] || {}
22
+ nil
17
23
  end
18
24
  end
19
25
  end
@@ -13,10 +13,10 @@ module Grape
13
13
  def_delegators :to_regexp, :===
14
14
  alias match? ===
15
15
 
16
- def initialize(origin, suffix, options)
16
+ def initialize(origin:, suffix:, anchor:, params:, format:, version:, requirements:)
17
17
  @origin = origin
18
- @path = build_path(origin, options[:anchor], suffix)
19
- @pattern = build_pattern(@path, options[:params], options[:format], options[:version], options[:requirements])
18
+ @path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
19
+ @pattern = Mustermann::Grape.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements))
20
20
  @to_regexp = @pattern.to_regexp
21
21
  end
22
22
 
@@ -28,24 +28,10 @@ module Grape
28
28
 
29
29
  private
30
30
 
31
- def build_pattern(path, params, format, version, requirements)
32
- Mustermann::Grape.new(
33
- path,
34
- uri_decode: true,
35
- params: params,
36
- capture: extract_capture(format, version, requirements)
37
- )
38
- end
39
-
40
- def build_path(pattern, anchor, suffix)
41
- PatternCache[[build_path_from_pattern(pattern, anchor), suffix]]
42
- end
43
-
44
31
  def extract_capture(format, version, requirements)
45
- capture = {}.tap do |h|
46
- h[:format] = map_str(format) if format.present?
47
- h[:version] = map_str(version) if version.present?
48
- end
32
+ capture = {}
33
+ capture[:format] = map_str(format) if format.present?
34
+ capture[:version] = map_str(version) if version.present?
49
35
 
50
36
  return capture if requirements.blank?
51
37
 
@@ -8,25 +8,21 @@ module Grape
8
8
  FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
9
9
  NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }
10
10
 
11
- attr_reader :app, :request_method
11
+ attr_reader :app, :request_method, :index
12
12
 
13
- def_delegators :pattern, :path, :origin
13
+ def_delegators :@app, :call
14
14
 
15
- def initialize(method, origin, path, options)
15
+ def initialize(endpoint, method, pattern, options)
16
+ super(pattern, options)
17
+ @app = endpoint
16
18
  @request_method = upcase_method(method)
17
- @pattern = Grape::Router::Pattern.new(origin, path, options)
18
19
  @match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
19
- super(options)
20
20
  end
21
21
 
22
22
  def convert_to_head_request!
23
23
  @request_method = Rack::HEAD
24
24
  end
25
25
 
26
- def exec(env)
27
- @app.call(env)
28
- end
29
-
30
26
  def apply(app)
31
27
  @app = app
32
28
  self
@@ -42,7 +38,7 @@ module Grape
42
38
  return params_without_input if input.blank?
43
39
 
44
40
  parsed = pattern.params(input)
45
- return {} unless parsed
41
+ return unless parsed
46
42
 
47
43
  parsed.compact.symbolize_keys
48
44
  end
@@ -50,7 +46,7 @@ module Grape
50
46
  private
51
47
 
52
48
  def params_without_input
53
- @params_without_input ||= pattern.captures_default.merge(attributes.params)
49
+ @params_without_input ||= pattern.captures_default.merge(options[:params])
54
50
  end
55
51
 
56
52
  def upcase_method(method)
data/lib/grape/router.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Grape
4
4
  class Router
5
- attr_reader :map, :compiled
6
-
7
5
  # Taken from Rails
8
6
  # normalize_path("/foo") # => "/foo"
9
7
  # normalize_path("/foo/") # => "/foo"
@@ -12,14 +10,15 @@ module Grape
12
10
  # normalize_path("/%ab") # => "/%AB"
13
11
  # https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19
14
12
  def self.normalize_path(path)
15
- return +'/' unless path
13
+ return '/' unless path
14
+ return path if path == '/'
16
15
 
17
16
  # Fast path for the overwhelming majority of paths that don't need to be normalized
18
- return path.dup if path == '/' || (path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//})))
17
+ return path if path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//}))
19
18
 
20
19
  # Slow path
21
20
  encoding = path.encoding
22
- path = +"/#{path}"
21
+ path = "/#{path}"
23
22
  path.squeeze!('/')
24
23
 
25
24
  unless path == '/'
@@ -38,14 +37,14 @@ module Grape
38
37
  end
39
38
 
40
39
  def compile!
41
- return if compiled
40
+ return if @compiled
42
41
 
43
42
  @union = Regexp.union(@neutral_regexes)
44
43
  @neutral_regexes = nil
45
44
  (Grape::HTTP_SUPPORTED_METHODS + ['*']).each do |method|
46
- next unless map.key?(method)
45
+ next unless @map.key?(method)
47
46
 
48
- routes = map[method]
47
+ routes = @map[method]
49
48
  optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
50
49
  @optimized_map[method] = Regexp.union(optimized_map)
51
50
  end
@@ -53,20 +52,20 @@ module Grape
53
52
  end
54
53
 
55
54
  def append(route)
56
- map[route.request_method] << route
55
+ @map[route.request_method] << route
57
56
  end
58
57
 
59
- def associate_routes(pattern, options)
60
- Grape::Router::GreedyRoute.new(pattern, options).then do |greedy_route|
61
- @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
62
- @neutral_map << greedy_route
63
- end
58
+ def associate_routes(greedy_route)
59
+ @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
60
+ @neutral_map << greedy_route
64
61
  end
65
62
 
66
63
  def call(env)
67
64
  with_optimization do
68
- response, route = identity(env)
69
- response || rotation(env, route)
65
+ input = Router.normalize_path(env[Rack::PATH_INFO])
66
+ method = env[Rack::REQUEST_METHOD]
67
+ response, route = identity(input, method, env)
68
+ response || rotation(input, method, env, route)
70
69
  end
71
70
  end
72
71
 
@@ -77,33 +76,33 @@ module Grape
77
76
  any.endpoint
78
77
  end
79
78
 
79
+ DEFAULT_RESPONSE_HEADERS = Grape::Util::Header.new.merge('X-Cascade' => 'pass').freeze
80
+ DEFAULT_RESPONSE_BODY = ['404 Not Found'].freeze
81
+
80
82
  private
81
83
 
82
- def identity(env)
84
+ def identity(input, method, env)
83
85
  route = nil
84
- response = transaction(env) do |input, method|
86
+ response = transaction(input, method, env) do
85
87
  route = match?(input, method)
86
- process_route(route, env) if route
88
+ process_route(route, input, env) if route
87
89
  end
88
90
  [response, route]
89
91
  end
90
92
 
91
- def rotation(env, exact_route = nil)
93
+ def rotation(input, method, env, exact_route)
92
94
  response = nil
93
- input, method = *extract_input_and_method(env)
94
- map[method].each do |route|
95
+ @map[method].each do |route|
95
96
  next if exact_route == route
96
97
  next unless route.match?(input)
97
98
 
98
- response = process_route(route, env)
99
+ response = process_route(route, input, env)
99
100
  break unless cascade?(response)
100
101
  end
101
102
  response
102
103
  end
103
104
 
104
- def transaction(env)
105
- input, method = *extract_input_and_method(env)
106
-
105
+ def transaction(input, method, env)
107
106
  # using a Proc is important since `return` will exit the enclosing function
108
107
  cascade_or_return_response = proc do |response|
109
108
  if response
@@ -111,53 +110,46 @@ module Grape
111
110
  return response unless cascade
112
111
 
113
112
  # we need to close the body if possible before dismissing
114
- response[2].try(:close)
113
+ response[2].close if response[2].respond_to?(:close)
115
114
  end
116
115
  end
117
116
  end
118
117
 
119
- last_response_cascade = cascade_or_return_response.call(yield(input, method))
118
+ response = yield
119
+ last_response_cascade = cascade_or_return_response.call(response)
120
120
  last_neighbor_route = greedy_match?(input)
121
121
 
122
122
  # If last_neighbor_route exists and request method is OPTIONS,
123
- # return response by using #call_with_allow_headers.
124
- return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Rack::OPTIONS && !last_response_cascade
123
+ # return response by using #include_allow_header.
124
+ return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && method == Rack::OPTIONS && last_neighbor_route
125
125
 
126
126
  route = match?(input, '*')
127
127
 
128
- return last_neighbor_route.options[:endpoint].call(env) if last_neighbor_route && last_response_cascade && route
128
+ return last_neighbor_route.call(env) if last_neighbor_route && last_response_cascade && route
129
129
 
130
- last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route
130
+ last_response_cascade = cascade_or_return_response.call(process_route(route, input, env)) if route
131
131
 
132
- return call_with_allow_headers(env, last_neighbor_route) if !last_response_cascade && last_neighbor_route
132
+ return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && last_neighbor_route
133
133
 
134
134
  nil
135
135
  end
136
136
 
137
- def process_route(route, env)
138
- prepare_env_from_route(env, route)
139
- route.exec(env)
140
- end
141
-
142
- def make_routing_args(default_args, route, input)
143
- args = default_args || { route_info: route }
144
- args.merge(route.params(input))
145
- end
146
-
147
- def extract_input_and_method(env)
148
- input = string_for(env[Rack::PATH_INFO])
149
- method = env[Rack::REQUEST_METHOD]
150
- [input, method]
137
+ def process_route(route, input, env, include_allow_header: false)
138
+ args = env[Grape::Env::GRAPE_ROUTING_ARGS] || { route_info: route }
139
+ route_params = route.params(input)
140
+ routing_args = args.merge(route_params || {})
141
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args
142
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
143
+ route.call(env)
151
144
  end
152
145
 
153
146
  def with_optimization
154
- compile! unless compiled
147
+ compile!
155
148
  yield || default_response
156
149
  end
157
150
 
158
151
  def default_response
159
- headers = Grape::Util::Header.new.merge('X-Cascade' => 'pass')
160
- [404, headers, ['404 Not Found']]
152
+ [404, DEFAULT_RESPONSE_HEADERS.dup, DEFAULT_RESPONSE_BODY.dup]
161
153
  end
162
154
 
163
155
  def match?(input, method)
@@ -168,23 +160,8 @@ module Grape
168
160
  @union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_index] } }
169
161
  end
170
162
 
171
- def call_with_allow_headers(env, route)
172
- prepare_env_from_route(env, route)
173
- env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.options[:allow_header]
174
- route.options[:endpoint].call(env)
175
- end
176
-
177
- def prepare_env_from_route(env, route)
178
- input, = *extract_input_and_method(env)
179
- env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
180
- end
181
-
182
163
  def cascade?(response)
183
164
  response && response[1]['X-Cascade'] == 'pass'
184
165
  end
185
-
186
- def string_for(input)
187
- self.class.normalize_path(input)
188
- end
189
166
  end
190
167
  end