grape 2.0.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +151 -1
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +404 -334
  5. data/UPGRADING.md +279 -7
  6. data/grape.gemspec +8 -8
  7. data/lib/grape/api/instance.rb +34 -66
  8. data/lib/grape/api.rb +47 -70
  9. data/lib/grape/content_types.rb +13 -10
  10. data/lib/grape/cookies.rb +31 -24
  11. data/lib/grape/dry_types.rb +0 -2
  12. data/lib/grape/dsl/api.rb +0 -2
  13. data/lib/grape/dsl/desc.rb +49 -44
  14. data/lib/grape/dsl/headers.rb +2 -2
  15. data/lib/grape/dsl/helpers.rb +8 -4
  16. data/lib/grape/dsl/inside_route.rb +67 -54
  17. data/lib/grape/dsl/parameters.rb +10 -9
  18. data/lib/grape/dsl/request_response.rb +14 -18
  19. data/lib/grape/dsl/routing.rb +34 -17
  20. data/lib/grape/dsl/validations.rb +13 -0
  21. data/lib/grape/endpoint.rb +120 -118
  22. data/lib/grape/{util/env.rb → env.rb} +0 -5
  23. data/lib/grape/error_formatter/base.rb +51 -21
  24. data/lib/grape/error_formatter/json.rb +7 -15
  25. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  26. data/lib/grape/error_formatter/txt.rb +11 -17
  27. data/lib/grape/error_formatter/xml.rb +3 -13
  28. data/lib/grape/error_formatter.rb +5 -25
  29. data/lib/grape/exceptions/base.rb +18 -30
  30. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  31. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  32. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  33. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  34. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  35. data/lib/grape/exceptions/validation.rb +5 -6
  36. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  37. data/lib/grape/exceptions/validation_errors.rb +4 -6
  38. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +2 -5
  39. data/lib/grape/extensions/hash.rb +7 -2
  40. data/lib/grape/extensions/hashie/mash.rb +3 -5
  41. data/lib/grape/formatter/base.rb +16 -0
  42. data/lib/grape/formatter/json.rb +4 -6
  43. data/lib/grape/formatter/serializable_hash.rb +1 -1
  44. data/lib/grape/formatter/txt.rb +3 -5
  45. data/lib/grape/formatter/xml.rb +4 -6
  46. data/lib/grape/formatter.rb +7 -25
  47. data/lib/grape/{util/json.rb → json.rb} +1 -3
  48. data/lib/grape/locale/en.yml +46 -42
  49. data/lib/grape/middleware/auth/base.rb +11 -34
  50. data/lib/grape/middleware/auth/dsl.rb +23 -31
  51. data/lib/grape/middleware/base.rb +41 -23
  52. data/lib/grape/middleware/error.rb +77 -76
  53. data/lib/grape/middleware/formatter.rb +48 -79
  54. data/lib/grape/middleware/globals.rb +1 -3
  55. data/lib/grape/middleware/stack.rb +26 -37
  56. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  57. data/lib/grape/middleware/versioner/base.rb +74 -0
  58. data/lib/grape/middleware/versioner/header.rb +59 -126
  59. data/lib/grape/middleware/versioner/param.rb +4 -25
  60. data/lib/grape/middleware/versioner/path.rb +10 -34
  61. data/lib/grape/middleware/versioner.rb +7 -14
  62. data/lib/grape/namespace.rb +4 -5
  63. data/lib/grape/params_builder/base.rb +18 -0
  64. data/lib/grape/params_builder/hash.rb +11 -0
  65. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  66. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  67. data/lib/grape/params_builder.rb +32 -0
  68. data/lib/grape/parser/base.rb +16 -0
  69. data/lib/grape/parser/json.rb +6 -8
  70. data/lib/grape/parser/xml.rb +6 -8
  71. data/lib/grape/parser.rb +5 -23
  72. data/lib/grape/path.rb +38 -60
  73. data/lib/grape/request.rb +161 -30
  74. data/lib/grape/router/base_route.rb +39 -0
  75. data/lib/grape/router/greedy_route.rb +20 -0
  76. data/lib/grape/router/pattern.rb +45 -31
  77. data/lib/grape/router/route.rb +28 -57
  78. data/lib/grape/router.rb +56 -43
  79. data/lib/grape/util/base_inheritable.rb +4 -4
  80. data/lib/grape/util/cache.rb +0 -3
  81. data/lib/grape/util/endpoint_configuration.rb +1 -1
  82. data/lib/grape/util/header.rb +13 -0
  83. data/lib/grape/util/inheritable_values.rb +0 -2
  84. data/lib/grape/util/lazy/block.rb +29 -0
  85. data/lib/grape/util/lazy/value.rb +38 -0
  86. data/lib/grape/util/lazy/value_array.rb +21 -0
  87. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  88. data/lib/grape/util/lazy/value_hash.rb +21 -0
  89. data/lib/grape/util/media_type.rb +70 -0
  90. data/lib/grape/util/registry.rb +27 -0
  91. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  92. data/lib/grape/util/stackable_values.rb +1 -6
  93. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  94. data/lib/grape/validations/attributes_doc.rb +38 -36
  95. data/lib/grape/validations/attributes_iterator.rb +1 -0
  96. data/lib/grape/validations/contract_scope.rb +34 -0
  97. data/lib/grape/validations/params_scope.rb +36 -32
  98. data/lib/grape/validations/types/array_coercer.rb +0 -2
  99. data/lib/grape/validations/types/dry_type_coercer.rb +9 -15
  100. data/lib/grape/validations/types/json.rb +0 -2
  101. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  102. data/lib/grape/validations/types/set_coercer.rb +0 -3
  103. data/lib/grape/validations/types.rb +0 -3
  104. data/lib/grape/validations/validator_factory.rb +2 -2
  105. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  106. data/lib/grape/validations/validators/base.rb +8 -11
  107. data/lib/grape/validations/validators/coerce_validator.rb +1 -1
  108. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  109. data/lib/grape/validations/validators/default_validator.rb +6 -2
  110. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  111. data/lib/grape/validations/validators/except_values_validator.rb +2 -2
  112. data/lib/grape/validations/validators/length_validator.rb +49 -0
  113. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  114. data/lib/grape/validations/validators/regexp_validator.rb +2 -2
  115. data/lib/grape/validations/validators/values_validator.rb +20 -57
  116. data/lib/grape/validations.rb +8 -21
  117. data/lib/grape/version.rb +1 -1
  118. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  119. data/lib/grape.rb +42 -274
  120. metadata +45 -44
  121. data/lib/grape/eager_load.rb +0 -20
  122. data/lib/grape/http/headers.rb +0 -71
  123. data/lib/grape/middleware/helpers.rb +0 -12
  124. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  125. data/lib/grape/router/attribute_translator.rb +0 -63
  126. data/lib/grape/util/lazy_block.rb +0 -27
  127. data/lib/grape/util/lazy_object.rb +0 -43
  128. data/lib/grape/util/lazy_value.rb +0 -91
  129. data/lib/grape/util/registrable.rb +0 -15
  130. data/lib/grape/validations/types/build_coercer.rb +0 -94
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  module Versioner
@@ -20,44 +18,19 @@ module Grape
20
18
  # route.
21
19
  class AcceptVersionHeader < Base
22
20
  def before
23
- potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
24
-
25
- if strict? && potential_version.empty?
26
- # If no Accept-Version header:
27
- throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
28
- end
29
-
30
- return if potential_version.empty?
21
+ potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub)
22
+ not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
31
23
 
32
- # If the requested version is not supported:
33
- throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version }
24
+ return if potential_version.blank?
34
25
 
26
+ not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version)
35
27
  env[Grape::Env::API_VERSION] = potential_version
36
28
  end
37
29
 
38
30
  private
39
31
 
40
- def versions
41
- options[:versions] || []
42
- end
43
-
44
- def strict?
45
- options[:version_options] && options[:version_options][:strict]
46
- end
47
-
48
- # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
49
- # of routes (see Grape::Router) for more information). To prevent
50
- # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
51
- def cascade?
52
- if options[:version_options]&.key?(:cascade)
53
- options[:version_options][:cascade]
54
- else
55
- true
56
- end
57
- end
58
-
59
- def error_headers
60
- cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
32
+ def not_acceptable!(message)
33
+ throw :error, status: 406, headers: error_headers, message: message
61
34
  end
62
35
  end
63
36
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ class Base < Grape::Middleware::Base
7
+ DEFAULT_OPTIONS = {
8
+ pattern: /.*/i.freeze,
9
+ version_options: {
10
+ strict: false,
11
+ cascade: true,
12
+ parameter: 'apiver'
13
+ }.freeze
14
+ }.freeze
15
+
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
28
+
29
+ def mount_path
30
+ options[:mount_path]
31
+ end
32
+
33
+ def pattern
34
+ options[:pattern]
35
+ end
36
+
37
+ def version_options
38
+ options[:version_options]
39
+ end
40
+
41
+ def strict?
42
+ version_options[:strict]
43
+ end
44
+
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]
50
+ end
51
+
52
+ def parameter_key
53
+ version_options[:parameter]
54
+ end
55
+
56
+ def vendor
57
+ version_options[:vendor]
58
+ end
59
+
60
+ def error_headers
61
+ cascade? ? { 'X-Cascade' => 'pass' } : {}
62
+ end
63
+
64
+ def potential_version_match?(potential_version)
65
+ versions.blank? || versions.any? { |v| v.to_s == potential_version }
66
+ end
67
+
68
+ def version_not_found!
69
+ throw :error, status: 404, message: '404 API Version Not Found', headers: { 'X-Cascade' => 'pass' }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
- require 'grape/middleware/versioner/parse_media_type_patch'
5
-
6
3
  module Grape
7
4
  module Middleware
8
5
  module Versioner
@@ -25,168 +22,104 @@ module Grape
25
22
  # X-Cascade header to alert Grape::Router to attempt the next matched
26
23
  # route.
27
24
  class Header < Base
28
- VENDOR_VERSION_HEADER_REGEX =
29
- /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze
30
-
31
- HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#{Regexp.last_match(0)}\^]+/.freeze
32
- HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))+/.freeze
33
-
34
25
  def before
35
- strict_header_checks if strict?
36
-
37
- if media_type || env[Grape::Env::GRAPE_ALLOWED_METHODS]
38
- media_type_header_handler
39
- elsif headers_contain_wrong_vendor?
40
- fail_with_invalid_accept_header!('API vendor not found.')
41
- elsif headers_contain_wrong_version?
42
- fail_with_invalid_version_header!('API version not found.')
26
+ match_best_quality_media_type! do |media_type|
27
+ env.update(
28
+ Grape::Env::API_TYPE => media_type.type,
29
+ Grape::Env::API_SUBTYPE => media_type.subtype,
30
+ Grape::Env::API_VENDOR => media_type.vendor,
31
+ Grape::Env::API_VERSION => media_type.version,
32
+ Grape::Env::API_FORMAT => media_type.format
33
+ )
43
34
  end
44
35
  end
45
36
 
46
37
  private
47
38
 
48
- def strict_header_checks
49
- strict_accept_header_presence_check
50
- strict_version_vendor_accept_header_presence_check
51
- end
52
-
53
- def strict_accept_header_presence_check
54
- return unless header.qvalues.empty?
55
-
56
- fail_with_invalid_accept_header!('Accept header must be set.')
57
- end
58
-
59
- def strict_version_vendor_accept_header_presence_check
60
- return if versions.blank? || an_accept_header_with_version_and_vendor_is_present?
61
-
62
- fail_with_invalid_accept_header!('API vendor or version not found.')
63
- end
39
+ def match_best_quality_media_type!
40
+ return unless vendor
64
41
 
65
- def an_accept_header_with_version_and_vendor_is_present?
66
- header.qvalues.keys.any? do |h|
67
- VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', ''))
42
+ strict_header_checks!
43
+ media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types)
44
+ if media_type
45
+ yield media_type
46
+ else
47
+ fail!
68
48
  end
69
49
  end
70
50
 
71
- def header
72
- @header ||= rack_accept_header
73
- end
74
-
75
- def media_type
76
- @media_type ||= header.best_of(available_media_types)
51
+ def accept_header
52
+ env['HTTP_ACCEPT']
77
53
  end
78
54
 
79
- def media_type_header_handler
80
- type, subtype = Rack::Accept::Header.parse_media_type(media_type)
81
- env[Grape::Env::API_TYPE] = type
82
- env[Grape::Env::API_SUBTYPE] = subtype
55
+ def strict_header_checks!
56
+ return unless strict?
83
57
 
84
- return unless VENDOR_VERSION_HEADER_REGEX =~ subtype
85
-
86
- env[Grape::Env::API_VENDOR] = Regexp.last_match[1]
87
- env[Grape::Env::API_VERSION] = Regexp.last_match[2]
88
- # weird that Grape::Middleware::Formatter also does this
89
- env[Grape::Env::API_FORMAT] = Regexp.last_match[3]
58
+ accept_header_check!
59
+ version_and_vendor_check!
90
60
  end
91
61
 
92
- def fail_with_invalid_accept_header!(message)
93
- raise Grape::Exceptions::InvalidAcceptHeader
94
- .new(message, error_headers)
95
- end
62
+ def accept_header_check!
63
+ return if accept_header.present?
96
64
 
97
- def fail_with_invalid_version_header!(message)
98
- raise Grape::Exceptions::InvalidVersionHeader
99
- .new(message, error_headers)
65
+ invalid_accept_header!('Accept header must be set.')
100
66
  end
101
67
 
102
- def available_media_types
103
- [].tap do |available_media_types|
104
- content_types.each_key do |extension|
105
- versions.reverse_each do |version|
106
- available_media_types << "application/vnd.#{vendor}-#{version}+#{extension}"
107
- available_media_types << "application/vnd.#{vendor}-#{version}"
108
- end
109
- available_media_types << "application/vnd.#{vendor}+#{extension}"
110
- end
68
+ def version_and_vendor_check!
69
+ return if versions.blank? || version_and_vendor?
111
70
 
112
- available_media_types << "application/vnd.#{vendor}"
113
- available_media_types.concat(content_types.values.flatten)
114
- end
71
+ invalid_accept_header!('API vendor or version not found.')
115
72
  end
116
73
 
117
- def headers_contain_wrong_vendor?
118
- header.values.all? do |header_value|
119
- vendor?(header_value) && request_vendor(header_value) != vendor
120
- end
74
+ def q_values_mime_types
75
+ @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
121
76
  end
122
77
 
123
- def headers_contain_wrong_version?
124
- header.values.all? do |header_value|
125
- version?(header_value) && versions.exclude?(request_version(header_value))
126
- end
78
+ def version_and_vendor?
79
+ q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
127
80
  end
128
81
 
129
- def rack_accept_header
130
- Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
131
- rescue RuntimeError => e
132
- fail_with_invalid_accept_header!(e.message)
82
+ def invalid_accept_header!(message)
83
+ raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
133
84
  end
134
85
 
135
- def versions
136
- options[:versions] || []
86
+ def invalid_version_header!(message)
87
+ raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
137
88
  end
138
89
 
139
- def vendor
140
- version_options && version_options[:vendor]
141
- end
90
+ def fail!
91
+ return if env[Grape::Env::GRAPE_ALLOWED_METHODS].present?
142
92
 
143
- def strict?
144
- version_options && version_options[:strict]
93
+ media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
94
+ vendor_not_found!(media_types) || version_not_found!(media_types)
145
95
  end
146
96
 
147
- def version_options
148
- options[:version_options]
149
- end
97
+ def vendor_not_found!(media_types)
98
+ return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }
150
99
 
151
- # By default those errors contain an `X-Cascade` header set to `pass`,
152
- # which allows nesting and stacking of routes
153
- # (see Grape::Router for more
154
- # information). To prevent # this behavior, and not add the `X-Cascade`
155
- # header, one can set the `:cascade` option to `false`.
156
- def cascade?
157
- if version_options&.key?(:cascade)
158
- version_options[:cascade]
159
- else
160
- true
161
- end
100
+ invalid_accept_header!('API vendor not found.')
162
101
  end
163
102
 
164
- def error_headers
165
- cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
166
- end
103
+ def version_not_found!(media_types)
104
+ return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) }
167
105
 
168
- # @param [String] media_type a content type
169
- # @return [Boolean] whether the content type sets a vendor
170
- def vendor?(media_type)
171
- _, subtype = Rack::Accept::Header.parse_media_type(media_type)
172
- subtype.present? && subtype[HAS_VENDOR_REGEX]
106
+ invalid_version_header!('API version not found.')
173
107
  end
174
108
 
175
- def request_vendor(media_type)
176
- _, subtype = Rack::Accept::Header.parse_media_type(media_type)
177
- subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
178
- end
179
-
180
- def request_version(media_type)
181
- _, subtype = Rack::Accept::Header.parse_media_type(media_type)
182
- subtype.match(VENDOR_VERSION_HEADER_REGEX)[2]
183
- end
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
184
119
 
185
- # @param [String] media_type a content type
186
- # @return [Boolean] whether the content type sets an API version
187
- def version?(media_type)
188
- _, subtype = Rack::Accept::Header.parse_media_type(media_type)
189
- subtype.present? && subtype[HAS_VERSION_REGEX]
120
+ available_media_types << base_media_type
121
+ available_media_types.concat(content_types.values.flatten)
122
+ end
190
123
  end
191
124
  end
192
125
  end
@@ -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
@@ -21,31 +19,12 @@ module Grape
21
19
  #
22
20
  # env['api.version'] => 'v1'
23
21
  class Param < Base
24
- def default_options
25
- {
26
- version_options: {
27
- parameter: 'apiver'
28
- }
29
- }
30
- end
31
-
32
22
  def before
33
- potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey]
34
- return if potential_version.nil?
35
-
36
- throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
37
- env[Grape::Env::API_VERSION] = potential_version
38
- env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH
39
- end
40
-
41
- private
42
-
43
- def paramkey
44
- version_options[:parameter] || default_options[:version_options][:parameter]
45
- end
23
+ potential_version = query_params[parameter_key]
24
+ return if potential_version.blank?
46
25
 
47
- def version_options
48
- options[:version_options]
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)
49
28
  end
50
29
  end
51
30
  end
@@ -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
@@ -19,44 +17,22 @@ module Grape
19
17
  # env['api.version'] => 'v1'
20
18
  #
21
19
  class Path < Base
22
- def default_options
23
- {
24
- pattern: /.*/i
25
- }
26
- end
27
-
28
20
  def before
29
- path = env[Grape::Http::Headers::PATH_INFO].dup
30
- path.sub!(mount_path, '') if mounted_path?(path)
21
+ path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
22
+ return if path_info == '/'
31
23
 
32
- if prefix && path.index(prefix) == 0 # rubocop:disable all
33
- path.sub!(prefix, '')
34
- path = Grape::Router.normalize_path(path)
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)
35
26
  end
36
27
 
37
- pieces = path.split('/')
38
- potential_version = pieces[1]
39
- return unless potential_version&.match?(options[:pattern])
40
-
41
- throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
42
- env[Grape::Env::API_VERSION] = potential_version
43
- end
44
-
45
- private
46
-
47
- def mounted_path?(path)
48
- return false unless mount_path && path.start_with?(mount_path)
49
-
50
- rest = path.slice(mount_path.length..-1)
51
- rest.start_with?('/') || rest.empty?
52
- end
28
+ slash_position = path_info.index('/', 1) # omit the first one
29
+ return unless slash_position
53
30
 
54
- def mount_path
55
- @mount_path ||= options[:mount_path] && options[:mount_path] != '/' ? options[:mount_path] : ''
56
- end
31
+ potential_version = path_info[1..slash_position - 1]
32
+ return unless potential_version.match?(pattern)
57
33
 
58
- def prefix
59
- Grape::Router.normalize_path(options[:prefix].to_s) if options[:prefix]
34
+ version_not_found! unless potential_version_match?(potential_version)
35
+ env[Grape::Env::API_VERSION] = potential_version
60
36
  end
61
37
  end
62
38
  end
@@ -4,30 +4,23 @@
4
4
  # on the requests. The current methods for determining version are:
5
5
  #
6
6
  # :header - version from HTTP Accept header.
7
+ # :accept_version_header - version from HTTP Accept-Version header
7
8
  # :path - version from uri. e.g. /v1/resource
8
9
  # :param - version from uri query string, e.g. /v1/resource?apiver=v1
9
- #
10
10
  # See individual classes for details.
11
11
  module Grape
12
12
  module Middleware
13
13
  module Versioner
14
+ extend Grape::Util::Registry
15
+
14
16
  module_function
15
17
 
16
- # @param strategy [Symbol] :path, :header or :param
18
+ # @param strategy [Symbol] :path, :header, :accept_version_header or :param
17
19
  # @return a middleware class based on strategy
18
20
  def using(strategy)
19
- case strategy
20
- when :path
21
- Path
22
- when :header
23
- Header
24
- when :param
25
- Param
26
- when :accept_version_header
27
- AcceptVersionHeader
28
- else
29
- raise Grape::Exceptions::InvalidVersionerOption.new(strategy)
30
- end
21
+ raise Grape::Exceptions::InvalidVersionerOption, strategy unless registry.key?(strategy)
22
+
23
+ registry[strategy]
31
24
  end
32
25
  end
33
26
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/util/cache'
4
-
5
3
  module Grape
6
4
  # A container for endpoints or other namespaces, which allows for both
7
5
  # logical grouping of endpoints as well as sharing common configuration.
@@ -14,7 +12,7 @@ module Grape
14
12
  # @option options :requirements [Hash] param-regex pairs, all of which must
15
13
  # be met by a request's params for all endpoints in this namespace, or
16
14
  # validation will fail and return a 422.
17
- def initialize(space, **options)
15
+ def initialize(space, options)
18
16
  @space = space.to_s
19
17
  @options = options
20
18
  end
@@ -33,13 +31,14 @@ module Grape
33
31
  # Join the namespaces from a list of settings to create a path prefix.
34
32
  # @param settings [Array] list of Grape::Util::InheritableSettings.
35
33
  def self.joined_space_path(settings)
36
- Grape::Router.normalize_path(JoinedSpaceCache[joined_space(settings)])
34
+ JoinedSpaceCache[joined_space(settings)]
37
35
  end
38
36
 
39
37
  class JoinedSpaceCache < Grape::Util::Cache
40
38
  def initialize
39
+ super
41
40
  @cache = Hash.new do |h, joined_space|
42
- h[joined_space] = -joined_space.join('/')
41
+ h[joined_space] = Grape::Router.normalize_path(joined_space.join('/'))
43
42
  end
44
43
  end
45
44
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class Base
6
+ class << self
7
+ def call(_params)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def inherited(klass)
12
+ super
13
+ ParamsBuilder.register(klass)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class Hash < Base
6
+ def self.call(params)
7
+ params.deep_symbolize_keys
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class HashWithIndifferentAccess < Base
6
+ def self.call(params)
7
+ params.with_indifferent_access
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class HashieMash < Base
6
+ def self.call(params)
7
+ ::Hashie::Mash.new(params)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ extend Grape::Util::Registry
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
+ module_function
14
+
15
+ 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)
25
+
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
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Parser
5
+ class Base
6
+ def self.call(_object, _env)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def self.inherited(klass)
11
+ super
12
+ Parser.register(klass)
13
+ end
14
+ end
15
+ end
16
+ end