grape 2.1.3 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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