grape 2.1.3 → 2.3.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +9 -7
  4. data/UPGRADING.md +27 -0
  5. data/grape.gemspec +7 -6
  6. data/lib/grape/api/instance.rb +22 -58
  7. data/lib/grape/api.rb +2 -11
  8. data/lib/grape/content_types.rb +13 -8
  9. data/lib/grape/dsl/desc.rb +27 -24
  10. data/lib/grape/dsl/helpers.rb +7 -3
  11. data/lib/grape/dsl/inside_route.rb +18 -24
  12. data/lib/grape/dsl/parameters.rb +2 -2
  13. data/lib/grape/dsl/request_response.rb +14 -18
  14. data/lib/grape/dsl/routing.rb +5 -12
  15. data/lib/grape/endpoint.rb +90 -82
  16. data/lib/grape/error_formatter/base.rb +51 -21
  17. data/lib/grape/error_formatter/json.rb +7 -15
  18. data/lib/grape/error_formatter/jsonapi.rb +7 -0
  19. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  20. data/lib/grape/error_formatter/txt.rb +13 -20
  21. data/lib/grape/error_formatter/xml.rb +3 -13
  22. data/lib/grape/error_formatter.rb +5 -25
  23. data/lib/grape/exceptions/base.rb +18 -30
  24. data/lib/grape/exceptions/validation.rb +5 -4
  25. data/lib/grape/exceptions/validation_errors.rb +2 -2
  26. data/lib/grape/formatter/base.rb +16 -0
  27. data/lib/grape/formatter/json.rb +4 -6
  28. data/lib/grape/formatter/serializable_hash.rb +1 -1
  29. data/lib/grape/formatter/txt.rb +3 -5
  30. data/lib/grape/formatter/xml.rb +4 -6
  31. data/lib/grape/formatter.rb +7 -25
  32. data/lib/grape/http/headers.rb +1 -0
  33. data/lib/grape/locale/en.yml +1 -0
  34. data/lib/grape/middleware/base.rb +14 -13
  35. data/lib/grape/middleware/error.rb +13 -9
  36. data/lib/grape/middleware/formatter.rb +3 -3
  37. data/lib/grape/middleware/versioner/accept_version_header.rb +7 -30
  38. data/lib/grape/middleware/versioner/base.rb +82 -0
  39. data/lib/grape/middleware/versioner/header.rb +89 -10
  40. data/lib/grape/middleware/versioner/param.rb +4 -22
  41. data/lib/grape/middleware/versioner/path.rb +10 -32
  42. data/lib/grape/middleware/versioner.rb +7 -14
  43. data/lib/grape/namespace.rb +1 -1
  44. data/lib/grape/parser/base.rb +16 -0
  45. data/lib/grape/parser/json.rb +6 -8
  46. data/lib/grape/parser/jsonapi.rb +7 -0
  47. data/lib/grape/parser/xml.rb +6 -8
  48. data/lib/grape/parser.rb +5 -23
  49. data/lib/grape/path.rb +39 -56
  50. data/lib/grape/request.rb +2 -2
  51. data/lib/grape/router/base_route.rb +2 -2
  52. data/lib/grape/router/greedy_route.rb +2 -2
  53. data/lib/grape/router/pattern.rb +23 -18
  54. data/lib/grape/router/route.rb +13 -5
  55. data/lib/grape/router.rb +5 -5
  56. data/lib/grape/util/registry.rb +27 -0
  57. data/lib/grape/validations/contract_scope.rb +2 -39
  58. data/lib/grape/validations/params_scope.rb +7 -11
  59. data/lib/grape/validations/types/dry_type_coercer.rb +10 -6
  60. data/lib/grape/validations/validator_factory.rb +2 -2
  61. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  62. data/lib/grape/validations/validators/base.rb +5 -9
  63. data/lib/grape/validations/validators/coerce_validator.rb +1 -1
  64. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  65. data/lib/grape/validations/validators/default_validator.rb +1 -1
  66. data/lib/grape/validations/validators/except_values_validator.rb +1 -1
  67. data/lib/grape/validations/validators/length_validator.rb +11 -4
  68. data/lib/grape/validations/validators/regexp_validator.rb +1 -1
  69. data/lib/grape/validations/validators/values_validator.rb +15 -57
  70. data/lib/grape/validations.rb +8 -17
  71. data/lib/grape/version.rb +1 -1
  72. data/lib/grape.rb +1 -1
  73. metadata +15 -12
  74. data/lib/grape/util/accept_header_handler.rb +0 -105
  75. data/lib/grape/util/registrable.rb +0 -15
  76. data/lib/grape/validations/types/build_coercer.rb +0 -92
@@ -2,11 +2,9 @@
2
2
 
3
3
  module Grape
4
4
  module Formatter
5
- module Txt
6
- class << self
7
- def call(object, _env)
8
- object.respond_to?(:to_txt) ? object.to_txt : object.to_s
9
- end
5
+ class Txt < Base
6
+ def self.call(object, _env)
7
+ object.respond_to?(:to_txt) ? object.to_txt : object.to_s
10
8
  end
11
9
  end
12
10
  end
@@ -2,13 +2,11 @@
2
2
 
3
3
  module Grape
4
4
  module Formatter
5
- module Xml
6
- class << self
7
- def call(object, _env)
8
- return object.to_xml if object.respond_to?(:to_xml)
5
+ class Xml < Base
6
+ def self.call(object, _env)
7
+ return object.to_xml if object.respond_to?(:to_xml)
9
8
 
10
- raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml')
11
- end
9
+ raise Grape::Exceptions::InvalidFormatter.new(object.class, 'xml')
12
10
  end
13
11
  end
14
12
  end
@@ -2,34 +2,16 @@
2
2
 
3
3
  module Grape
4
4
  module Formatter
5
- extend Util::Registrable
5
+ extend Grape::Util::Registry
6
6
 
7
- class << self
8
- def builtin_formatters
9
- @builtin_formatters ||= {
10
- json: Grape::Formatter::Json,
11
- jsonapi: Grape::Formatter::Json,
12
- serializable_hash: Grape::Formatter::SerializableHash,
13
- txt: Grape::Formatter::Txt,
14
- xml: Grape::Formatter::Xml
15
- }
16
- end
7
+ module_function
17
8
 
18
- def formatters(**options)
19
- builtin_formatters.merge(default_elements).merge!(options[:formatters] || {})
20
- end
9
+ DEFAULT_LAMBDA_FORMATTER = ->(obj, _env) { obj }
21
10
 
22
- def formatter_for(api_format, **options)
23
- spec = formatters(**options)[api_format]
24
- case spec
25
- when nil
26
- ->(obj, _env) { obj }
27
- when Symbol
28
- method(spec)
29
- else
30
- spec
31
- end
32
- end
11
+ def formatter_for(api_format, formatters)
12
+ return formatters[api_format] if formatters&.key?(api_format)
13
+
14
+ registry[api_format] || DEFAULT_LAMBDA_FORMATTER
33
15
  end
34
16
  end
35
17
  end
@@ -6,6 +6,7 @@ module Grape
6
6
  HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
7
7
  HTTP_ACCEPT = 'HTTP_ACCEPT'
8
8
  HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'
9
+ HTTP_VERSION = 'HTTP_VERSION'
9
10
 
10
11
  ALLOW = 'Allow'
11
12
  LOCATION = 'Location'
@@ -11,6 +11,7 @@ en:
11
11
  except_values: 'has a value not allowed'
12
12
  same_as: 'is not the same as %{parameter}'
13
13
  length: 'is expected to have length within %{min} and %{max}'
14
+ length_is: 'is expected to have length exactly equal to %{is}'
14
15
  length_min: 'is expected to have length greater than or equal to %{min}'
15
16
  length_max: 'is expected to have length less than or equal to %{max}'
16
17
  missing_vendor_option:
@@ -4,18 +4,17 @@ module Grape
4
4
  module Middleware
5
5
  class Base
6
6
  include Helpers
7
+ include Grape::DSL::Headers
7
8
 
8
9
  attr_reader :app, :env, :options
9
10
 
10
11
  TEXT_HTML = 'text/html'
11
12
 
12
- include Grape::DSL::Headers
13
-
14
13
  # @param [Rack Application] app The standard argument for a Rack middleware.
15
14
  # @param [Hash] options A hash of options, simply stored for use by subclasses.
16
15
  def initialize(app, *options)
17
16
  @app = app
18
- @options = options.any? ? default_options.merge(options.shift) : default_options
17
+ @options = options.any? ? default_options.deep_merge(options.shift) : default_options
19
18
  @app_response = nil
20
19
  end
21
20
 
@@ -61,22 +60,20 @@ module Grape
61
60
  @app_response = Rack::Response.new(@app_response[2], @app_response[0], @app_response[1])
62
61
  end
63
62
 
64
- def content_type_for(format)
65
- HashWithIndifferentAccess.new(content_types)[format]
63
+ def content_types
64
+ @content_types ||= Grape::ContentTypes.content_types_for(options[:content_types])
66
65
  end
67
66
 
68
- def content_types
69
- ContentTypes.content_types_for(options[:content_types])
67
+ def mime_types
68
+ @mime_types ||= Grape::ContentTypes.mime_types_for(content_types)
70
69
  end
71
70
 
72
- def content_type
73
- content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML
71
+ def content_type_for(format)
72
+ content_types_indifferent_access[format]
74
73
  end
75
74
 
76
- def mime_types
77
- @mime_types ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params|
78
- types_without_params[v.split(';').first] = k
79
- end
75
+ def content_type
76
+ content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML
80
77
  end
81
78
 
82
79
  private
@@ -89,6 +86,10 @@ module Grape
89
86
  when Array then response[1].merge!(headers)
90
87
  end
91
88
  end
89
+
90
+ def content_types_indifferent_access
91
+ @content_types_indifferent_access ||= content_types.with_indifferent_access
92
+ end
92
93
  end
93
94
  end
94
95
  end
@@ -26,7 +26,7 @@ module Grape
26
26
 
27
27
  def initialize(app, *options)
28
28
  super
29
- self.class.send(:include, @options[:helpers]) if @options[:helpers]
29
+ self.class.include(@options[:helpers]) if @options[:helpers]
30
30
  end
31
31
 
32
32
  def call!(env)
@@ -45,7 +45,7 @@ module Grape
45
45
 
46
46
  def format_message(message, backtrace, original_exception = nil)
47
47
  format = env[Grape::Env::API_FORMAT] || options[:format]
48
- formatter = Grape::ErrorFormatter.formatter_for(format, **options)
48
+ formatter = Grape::ErrorFormatter.formatter_for(format, options[:error_formatters], options[:default_error_formatter])
49
49
  return formatter.call(message, backtrace, options, env, original_exception) if formatter
50
50
 
51
51
  throw :error,
@@ -65,6 +65,7 @@ module Grape
65
65
 
66
66
  def error_response(error = {})
67
67
  status = error[:status] || options[:default_status]
68
+ env[Grape::Env::API_ENDPOINT].status(status) # error! may not have been called
68
69
  message = error[:message] || options[:default_message]
69
70
  headers = { Rack::CONTENT_TYPE => content_type }.tap do |h|
70
71
  h.merge!(error[:headers]) if error[:headers].is_a?(Hash)
@@ -79,7 +80,7 @@ module Grape
79
80
  end
80
81
 
81
82
  def rescue_handler_for_base_only_class(klass)
82
- error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err }
83
+ error, handler = options[:base_only_rescue_handlers]&.find { |err, _handler| klass == err }
83
84
 
84
85
  return unless error
85
86
 
@@ -87,7 +88,7 @@ module Grape
87
88
  end
88
89
 
89
90
  def rescue_handler_for_class_or_its_ancestor(klass)
90
- error, handler = options[:rescue_handlers].find { |err, _handler| klass <= err }
91
+ error, handler = options[:rescue_handlers]&.find { |err, _handler| klass <= err }
91
92
 
92
93
  return unless error
93
94
 
@@ -120,16 +121,17 @@ module Grape
120
121
  handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
121
122
  end
122
123
 
123
- response = error!(response[:message], response[:status], response[:headers]) if error?(response)
124
-
125
- if response.is_a?(Rack::Response)
124
+ if error?(response)
125
+ error_response(response)
126
+ elsif response.is_a?(Rack::Response)
126
127
  response
127
128
  else
128
- run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new, endpoint)
129
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
129
130
  end
130
131
  end
131
132
 
132
133
  def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
134
+ env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route
133
135
  rack_response(
134
136
  status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type),
135
137
  format_message(message, backtrace, original_exception)
@@ -137,7 +139,9 @@ module Grape
137
139
  end
138
140
 
139
141
  def error?(response)
140
- response.is_a?(Hash) && response[:message] && response[:status] && response[:headers]
142
+ return false unless response.is_a?(Hash)
143
+
144
+ response.key?(:message) && response.key?(:status) && response.key?(:headers)
141
145
  end
142
146
  end
143
147
  end
@@ -53,8 +53,8 @@ module Grape
53
53
  end
54
54
 
55
55
  def fetch_formatter(headers, options)
56
- api_format = mime_types[headers[Rack::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
57
- Grape::Formatter.formatter_for(api_format, **options)
56
+ api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
57
+ Grape::Formatter.formatter_for(api_format, options[:formatters])
58
58
  end
59
59
 
60
60
  # Set the content type header for the API format if it is not already present.
@@ -97,7 +97,7 @@ module Grape
97
97
  fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
98
98
 
99
99
  throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
100
- parser = Grape::Parser.parser_for fmt, **options
100
+ parser = Grape::Parser.parser_for fmt, options[:parsers]
101
101
  if parser
102
102
  begin
103
103
  body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
@@ -18,44 +18,21 @@ module Grape
18
18
  # route.
19
19
  class AcceptVersionHeader < Base
20
20
  def before
21
- potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
21
+ potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]
22
+ potential_version = potential_version.scrub unless potential_version.nil?
22
23
 
23
- if strict? && potential_version.empty?
24
- # If no Accept-Version header:
25
- throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
26
- end
24
+ not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
27
25
 
28
- return if potential_version.empty?
29
-
30
- # If the requested version is not supported:
31
- throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version }
26
+ return if potential_version.blank?
32
27
 
28
+ not_acceptable!('The requested version is not supported.') unless potential_version_match?(potential_version)
33
29
  env[Grape::Env::API_VERSION] = potential_version
34
30
  end
35
31
 
36
32
  private
37
33
 
38
- def versions
39
- options[:versions] || []
40
- end
41
-
42
- def strict?
43
- options[:version_options] && options[:version_options][:strict]
44
- end
45
-
46
- # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
47
- # of routes (see Grape::Router) for more information). To prevent
48
- # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
49
- def cascade?
50
- if options[:version_options]&.key?(:cascade)
51
- options[:version_options][:cascade]
52
- else
53
- true
54
- end
55
- end
56
-
57
- def error_headers
58
- cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
34
+ def not_acceptable!(message)
35
+ throw :error, status: 406, headers: error_headers, message: message
59
36
  end
60
37
  end
61
38
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module Versioner
6
+ class Base < Grape::Middleware::Base
7
+ DEFAULT_PATTERN = /.*/i.freeze
8
+ DEFAULT_PARAMETER = 'apiver'
9
+
10
+ def self.inherited(klass)
11
+ super
12
+ Versioner.register(klass)
13
+ end
14
+
15
+ def default_options
16
+ {
17
+ versions: nil,
18
+ prefix: nil,
19
+ mount_path: nil,
20
+ pattern: DEFAULT_PATTERN,
21
+ version_options: {
22
+ strict: false,
23
+ cascade: true,
24
+ parameter: DEFAULT_PARAMETER
25
+ }
26
+ }
27
+ end
28
+
29
+ def versions
30
+ options[:versions]
31
+ end
32
+
33
+ def prefix
34
+ options[:prefix]
35
+ end
36
+
37
+ def mount_path
38
+ options[:mount_path]
39
+ end
40
+
41
+ def pattern
42
+ options[:pattern]
43
+ end
44
+
45
+ def version_options
46
+ options[:version_options]
47
+ end
48
+
49
+ def strict?
50
+ version_options[:strict]
51
+ end
52
+
53
+ # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
54
+ # of routes (see Grape::Router) for more information). To prevent
55
+ # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
56
+ def cascade?
57
+ version_options[:cascade]
58
+ end
59
+
60
+ def parameter_key
61
+ version_options[:parameter]
62
+ end
63
+
64
+ def vendor
65
+ version_options[:vendor]
66
+ end
67
+
68
+ def error_headers
69
+ cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
70
+ end
71
+
72
+ def potential_version_match?(potential_version)
73
+ versions.blank? || versions.any? { |v| v.to_s == potential_version }
74
+ end
75
+
76
+ def version_not_found!
77
+ throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -23,16 +23,7 @@ module Grape
23
23
  # route.
24
24
  class Header < Base
25
25
  def before
26
- handler = Grape::Util::AcceptHeaderHandler.new(
27
- accept_header: env[Grape::Http::Headers::HTTP_ACCEPT],
28
- versions: options[:versions],
29
- **options.fetch(:version_options) { {} }
30
- )
31
-
32
- handler.match_best_quality_media_type!(
33
- content_types: content_types,
34
- allowed_methods: env[Grape::Env::GRAPE_ALLOWED_METHODS]
35
- ) do |media_type|
26
+ match_best_quality_media_type! do |media_type|
36
27
  env.update(
37
28
  Grape::Env::API_TYPE => media_type.type,
38
29
  Grape::Env::API_SUBTYPE => media_type.subtype,
@@ -42,6 +33,94 @@ module Grape
42
33
  )
43
34
  end
44
35
  end
36
+
37
+ private
38
+
39
+ def match_best_quality_media_type!
40
+ return unless vendor
41
+
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!
48
+ end
49
+ end
50
+
51
+ def accept_header
52
+ env[Grape::Http::Headers::HTTP_ACCEPT]
53
+ end
54
+
55
+ def strict_header_checks!
56
+ return unless strict?
57
+
58
+ accept_header_check!
59
+ version_and_vendor_check!
60
+ end
61
+
62
+ def accept_header_check!
63
+ return if accept_header.present?
64
+
65
+ invalid_accept_header!('Accept header must be set.')
66
+ end
67
+
68
+ def version_and_vendor_check!
69
+ return if versions.blank? || version_and_vendor?
70
+
71
+ invalid_accept_header!('API vendor or version not found.')
72
+ end
73
+
74
+ def q_values_mime_types
75
+ @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
76
+ end
77
+
78
+ def version_and_vendor?
79
+ q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
80
+ end
81
+
82
+ def invalid_accept_header!(message)
83
+ raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
84
+ end
85
+
86
+ def invalid_version_header!(message)
87
+ raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
88
+ end
89
+
90
+ def fail!
91
+ return if env[Grape::Env::GRAPE_ALLOWED_METHODS].present?
92
+
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)
95
+ end
96
+
97
+ def vendor_not_found!(media_types)
98
+ return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }
99
+
100
+ invalid_accept_header!('API vendor not found.')
101
+ end
102
+
103
+ def version_not_found!(media_types)
104
+ return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) }
105
+
106
+ invalid_version_header!('API version not found.')
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
45
124
  end
46
125
  end
47
126
  end
@@ -19,31 +19,13 @@ module Grape
19
19
  #
20
20
  # env['api.version'] => 'v1'
21
21
  class Param < Base
22
- def default_options
23
- {
24
- version_options: {
25
- parameter: 'apiver'
26
- }
27
- }
28
- end
29
-
30
22
  def before
31
- potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[paramkey]
32
- return if potential_version.nil?
23
+ potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key]
24
+ return if potential_version.blank?
33
25
 
34
- 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 }
26
+ version_not_found! unless potential_version_match?(potential_version)
35
27
  env[Grape::Env::API_VERSION] = potential_version
36
- env[Rack::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Rack::RACK_REQUEST_QUERY_HASH
37
- end
38
-
39
- private
40
-
41
- def paramkey
42
- version_options[:parameter] || default_options[:version_options][:parameter]
43
- end
44
-
45
- def version_options
46
- options[:version_options]
28
+ env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) if env.key? Rack::RACK_REQUEST_QUERY_HASH
47
29
  end
48
30
  end
49
31
  end
@@ -17,44 +17,22 @@ module Grape
17
17
  # env['api.version'] => 'v1'
18
18
  #
19
19
  class Path < Base
20
- def default_options
21
- {
22
- pattern: /.*/i
23
- }
24
- end
25
-
26
20
  def before
27
- path = env[Rack::PATH_INFO].dup
28
- path.sub!(mount_path, '') if mounted_path?(path)
21
+ path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
22
+ return if path_info == '/'
29
23
 
30
- if prefix && path.index(prefix) == 0 # rubocop:disable all
31
- path.sub!(prefix, '')
32
- 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)
33
26
  end
34
27
 
35
- pieces = path.split('/')
36
- potential_version = pieces[1]
37
- return unless potential_version&.match?(options[:pattern])
38
-
39
- throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
40
- env[Grape::Env::API_VERSION] = potential_version
41
- end
42
-
43
- private
28
+ slash_position = path_info.index('/', 1) # omit the first one
29
+ return unless slash_position
44
30
 
45
- def mounted_path?(path)
46
- return false unless mount_path && path.start_with?(mount_path)
31
+ potential_version = path_info[1..slash_position - 1]
32
+ return unless potential_version.match?(pattern)
47
33
 
48
- rest = path.slice(mount_path.length..-1)
49
- rest.start_with?('/') || rest.empty?
50
- end
51
-
52
- def mount_path
53
- @mount_path ||= options[:mount_path] && options[:mount_path] != '/' ? options[:mount_path] : ''
54
- end
55
-
56
- def prefix
57
- 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
58
36
  end
59
37
  end
60
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
@@ -12,7 +12,7 @@ module Grape
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, options)
16
16
  @space = space.to_s
17
17
  @options = options
18
18
  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