grape 2.1.3 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,45 +17,22 @@ module Grape
17
17
  # X-Cascade header to alert Grape::Router to attempt the next matched
18
18
  # route.
19
19
  class AcceptVersionHeader < Base
20
- def before
21
- potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
22
-
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
20
+ include VersionerHelpers
27
21
 
28
- return if potential_version.empty?
22
+ def before
23
+ potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]&.strip
24
+ not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
29
25
 
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
@@ -22,17 +22,10 @@ module Grape
22
22
  # X-Cascade header to alert Grape::Router to attempt the next matched
23
23
  # route.
24
24
  class Header < Base
25
+ include VersionerHelpers
26
+
25
27
  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|
28
+ match_best_quality_media_type! do |media_type|
36
29
  env.update(
37
30
  Grape::Env::API_TYPE => media_type.type,
38
31
  Grape::Env::API_SUBTYPE => media_type.subtype,
@@ -42,6 +35,98 @@ module Grape
42
35
  )
43
36
  end
44
37
  end
38
+
39
+ private
40
+
41
+ def match_best_quality_media_type!
42
+ return unless vendor
43
+
44
+ strict_header_checks!
45
+ media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types)
46
+ if media_type
47
+ yield media_type
48
+ else
49
+ fail!(allowed_methods)
50
+ end
51
+ end
52
+
53
+ def allowed_methods
54
+ env[Grape::Env::GRAPE_ALLOWED_METHODS]
55
+ end
56
+
57
+ def accept_header
58
+ env[Grape::Http::Headers::HTTP_ACCEPT]
59
+ end
60
+
61
+ def strict_header_checks!
62
+ return unless strict?
63
+
64
+ accept_header_check!
65
+ version_and_vendor_check!
66
+ end
67
+
68
+ def accept_header_check!
69
+ return if accept_header.present?
70
+
71
+ invalid_accept_header!('Accept header must be set.')
72
+ end
73
+
74
+ def version_and_vendor_check!
75
+ return if versions.blank? || version_and_vendor?
76
+
77
+ invalid_accept_header!('API vendor or version not found.')
78
+ end
79
+
80
+ def q_values_mime_types
81
+ @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
82
+ end
83
+
84
+ def version_and_vendor?
85
+ q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
86
+ end
87
+
88
+ def invalid_accept_header!(message)
89
+ raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
90
+ end
91
+
92
+ def invalid_version_header!(message)
93
+ raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
94
+ end
95
+
96
+ def fail!(grape_allowed_methods)
97
+ return grape_allowed_methods if grape_allowed_methods.present?
98
+
99
+ media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
100
+ vendor_not_found!(media_types) || version_not_found!(media_types)
101
+ end
102
+
103
+ def vendor_not_found!(media_types)
104
+ return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }
105
+
106
+ invalid_accept_header!('API vendor not found.')
107
+ end
108
+
109
+ def version_not_found!(media_types)
110
+ return unless media_types.all? { |media_type| media_type&.version && versions&.exclude?(media_type.version) }
111
+
112
+ invalid_version_header!('API version not found.')
113
+ end
114
+
115
+ def available_media_types
116
+ [].tap do |available_media_types|
117
+ base_media_type = "application/vnd.#{vendor}"
118
+ content_types.each_key do |extension|
119
+ versions&.reverse_each do |version|
120
+ available_media_types << "#{base_media_type}-#{version}+#{extension}"
121
+ available_media_types << "#{base_media_type}-#{version}"
122
+ end
123
+ available_media_types << "#{base_media_type}+#{extension}"
124
+ end
125
+
126
+ available_media_types << base_media_type
127
+ available_media_types.concat(content_types.values.flatten)
128
+ end
129
+ end
45
130
  end
46
131
  end
47
132
  end
@@ -19,31 +19,15 @@ 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
22
+ include VersionerHelpers
29
23
 
30
24
  def before
31
- potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[paramkey]
32
- return if potential_version.nil?
25
+ potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key]
26
+ return if potential_version.blank?
33
27
 
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 }
28
+ version_not_found! unless potential_version_match?(potential_version)
35
29
  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]
30
+ env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) if env.key? Rack::RACK_REQUEST_QUERY_HASH
47
31
  end
48
32
  end
49
33
  end
@@ -17,44 +17,24 @@ 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
20
+ include VersionerHelpers
25
21
 
26
22
  def before
27
- path = env[Rack::PATH_INFO].dup
28
- path.sub!(mount_path, '') if mounted_path?(path)
23
+ path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
24
+ return if path_info == '/'
29
25
 
30
- if prefix && path.index(prefix) == 0 # rubocop:disable all
31
- path.sub!(prefix, '')
32
- path = Grape::Router.normalize_path(path)
26
+ [mount_path, Grape::Router.normalize_path(prefix)].each do |path|
27
+ path_info.delete_prefix!(path) if path.present? && path != '/' && path_info.start_with?(path)
33
28
  end
34
29
 
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
30
+ slash_position = path_info.index('/', 1) # omit the first one
31
+ return unless slash_position
44
32
 
45
- def mounted_path?(path)
46
- return false unless mount_path && path.start_with?(mount_path)
33
+ potential_version = path_info[1..slash_position - 1]
34
+ return unless potential_version.match?(pattern)
47
35
 
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]
36
+ version_not_found! unless potential_version_match?(potential_version)
37
+ env[Grape::Env::API_VERSION] = potential_version
58
38
  end
59
39
  end
60
40
  end
@@ -4,30 +4,21 @@
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
14
  module_function
15
15
 
16
- # @param strategy [Symbol] :path, :header or :param
16
+ # @param strategy [Symbol] :path, :header, :accept_version_header or :param
17
17
  # @return a middleware class based on strategy
18
18
  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
19
+ Grape::Middleware::Versioner.const_get(:"#{strategy.to_s.camelize}")
20
+ rescue NameError
21
+ raise Grape::Exceptions::InvalidVersionerOption, strategy
31
22
  end
32
23
  end
33
24
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ module VersionerHelpers
6
+ DEFAULT_PATTERN = /.*/i.freeze
7
+ DEFAULT_PARAMETER = 'apiver'
8
+
9
+ def default_options
10
+ {
11
+ versions: nil,
12
+ prefix: nil,
13
+ mount_path: nil,
14
+ pattern: DEFAULT_PATTERN,
15
+ version_options: {
16
+ strict: false,
17
+ cascade: true,
18
+ parameter: DEFAULT_PARAMETER
19
+ }
20
+ }
21
+ end
22
+
23
+ def versions
24
+ options[:versions]
25
+ end
26
+
27
+ def prefix
28
+ options[:prefix]
29
+ end
30
+
31
+ def mount_path
32
+ options[:mount_path]
33
+ end
34
+
35
+ def pattern
36
+ options[:pattern]
37
+ end
38
+
39
+ def version_options
40
+ options[:version_options]
41
+ end
42
+
43
+ def strict?
44
+ version_options[:strict]
45
+ end
46
+
47
+ # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
48
+ # of routes (see Grape::Router) for more information). To prevent
49
+ # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
50
+ def cascade?
51
+ version_options[:cascade]
52
+ end
53
+
54
+ def parameter_key
55
+ version_options[:parameter]
56
+ end
57
+
58
+ def vendor
59
+ version_options[:vendor]
60
+ end
61
+
62
+ def error_headers
63
+ cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
64
+ end
65
+
66
+ def potential_version_match?(potential_version)
67
+ versions.blank? || versions.any? { |v| v.to_s == potential_version }
68
+ end
69
+
70
+ def version_not_found!
71
+ throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' }
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/grape/parser.rb CHANGED
@@ -2,32 +2,16 @@
2
2
 
3
3
  module Grape
4
4
  module Parser
5
- extend Util::Registrable
5
+ module_function
6
6
 
7
- class << self
8
- def builtin_parsers
9
- @builtin_parsers ||= {
10
- json: Grape::Parser::Json,
11
- jsonapi: Grape::Parser::Json,
12
- xml: Grape::Parser::Xml
13
- }
14
- end
7
+ DEFAULTS = {
8
+ json: Grape::Parser::Json,
9
+ jsonapi: Grape::Parser::Json,
10
+ xml: Grape::Parser::Xml
11
+ }.freeze
15
12
 
16
- def parsers(**options)
17
- builtin_parsers.merge(default_elements).merge!(options[:parsers] || {})
18
- end
19
-
20
- def parser_for(api_format, **options)
21
- spec = parsers(**options)[api_format]
22
- case spec
23
- when nil
24
- nil
25
- when Symbol
26
- method(spec)
27
- else
28
- spec
29
- end
30
- end
13
+ def parser_for(format, parsers = nil)
14
+ parsers&.key?(format) ? parsers[format] : DEFAULTS[format]
31
15
  end
32
16
  end
33
17
  end
@@ -7,20 +7,25 @@ module Grape
7
7
  def initialize(attrs, options, required, scope, **opts)
8
8
  @min = options[:min]
9
9
  @max = options[:max]
10
+ @is = options[:is]
10
11
 
11
12
  super
12
13
 
13
14
  raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?)
14
15
  raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?)
15
16
  raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max
17
+
18
+ return if @is.nil?
19
+ raise ArgumentError, 'is must be an integer greater than zero' if !@is.is_a?(Integer) || !@is.positive?
20
+ raise ArgumentError, 'is cannot be combined with min or max' if !@min.nil? || !@max.nil?
16
21
  end
17
22
 
18
23
  def validate_param!(attr_name, params)
19
24
  param = params[attr_name]
20
25
 
21
- raise ArgumentError, "parameter #{param} does not support #length" unless param.respond_to?(:length)
26
+ return unless param.respond_to?(:length)
22
27
 
23
- return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max)
28
+ return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) || (!@is.nil? && param.length != @is)
24
29
 
25
30
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message)
26
31
  end
@@ -32,8 +37,10 @@ module Grape
32
37
  format I18n.t(:length, scope: 'grape.errors.messages'), min: @min, max: @max
33
38
  elsif @min
34
39
  format I18n.t(:length_min, scope: 'grape.errors.messages'), min: @min
35
- else
40
+ elsif @max
36
41
  format I18n.t(:length_max, scope: 'grape.errors.messages'), max: @max
42
+ else
43
+ format I18n.t(:length_is, scope: 'grape.errors.messages'), is: @is
37
44
  end
38
45
  end
39
46
  end
data/lib/grape/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '2.1.3'
5
+ VERSION = '2.2.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.3
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Bleigh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-13 00:00:00.000000000 Z
11
+ date: 2024-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -172,6 +172,7 @@ files:
172
172
  - lib/grape/middleware/versioner/header.rb
173
173
  - lib/grape/middleware/versioner/param.rb
174
174
  - lib/grape/middleware/versioner/path.rb
175
+ - lib/grape/middleware/versioner_helpers.rb
175
176
  - lib/grape/namespace.rb
176
177
  - lib/grape/parser.rb
177
178
  - lib/grape/parser/json.rb
@@ -189,7 +190,6 @@ files:
189
190
  - lib/grape/serve_stream/sendfile_response.rb
190
191
  - lib/grape/serve_stream/stream_response.rb
191
192
  - lib/grape/types/invalid_value.rb
192
- - lib/grape/util/accept_header_handler.rb
193
193
  - lib/grape/util/base_inheritable.rb
194
194
  - lib/grape/util/cache.rb
195
195
  - lib/grape/util/endpoint_configuration.rb
@@ -203,7 +203,6 @@ files:
203
203
  - lib/grape/util/lazy/value_enumerable.rb
204
204
  - lib/grape/util/lazy/value_hash.rb
205
205
  - lib/grape/util/media_type.rb
206
- - lib/grape/util/registrable.rb
207
206
  - lib/grape/util/reverse_stackable_values.rb
208
207
  - lib/grape/util/stackable_values.rb
209
208
  - lib/grape/util/strict_hash_configuration.rb
@@ -251,9 +250,10 @@ licenses:
251
250
  - MIT
252
251
  metadata:
253
252
  bug_tracker_uri: https://github.com/ruby-grape/grape/issues
254
- changelog_uri: https://github.com/ruby-grape/grape/blob/v2.1.3/CHANGELOG.md
255
- documentation_uri: https://www.rubydoc.info/gems/grape/2.1.3
256
- source_code_uri: https://github.com/ruby-grape/grape/tree/v2.1.3
253
+ changelog_uri: https://github.com/ruby-grape/grape/blob/v2.2.0/CHANGELOG.md
254
+ documentation_uri: https://www.rubydoc.info/gems/grape/2.2.0
255
+ source_code_uri: https://github.com/ruby-grape/grape/tree/v2.2.0
256
+ rubygems_mfa_required: 'true'
257
257
  post_install_message:
258
258
  rdoc_options: []
259
259
  require_paths:
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grape
4
- module Util
5
- class AcceptHeaderHandler
6
- attr_reader :accept_header, :versions, :vendor, :strict, :cascade
7
-
8
- def initialize(accept_header:, versions:, **options)
9
- @accept_header = accept_header
10
- @versions = versions
11
- @vendor = options.fetch(:vendor, nil)
12
- @strict = options.fetch(:strict, false)
13
- @cascade = options.fetch(:cascade, true)
14
- end
15
-
16
- def match_best_quality_media_type!(content_types: Grape::ContentTypes::CONTENT_TYPES, allowed_methods: nil)
17
- return unless vendor
18
-
19
- strict_header_checks!
20
- media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types(content_types))
21
- if media_type
22
- yield media_type
23
- else
24
- fail!(allowed_methods)
25
- end
26
- end
27
-
28
- private
29
-
30
- def strict_header_checks!
31
- return unless strict
32
-
33
- accept_header_check!
34
- version_and_vendor_check!
35
- end
36
-
37
- def accept_header_check!
38
- return if accept_header.present?
39
-
40
- invalid_accept_header!('Accept header must be set.')
41
- end
42
-
43
- def version_and_vendor_check!
44
- return if versions.blank? || version_and_vendor?
45
-
46
- invalid_accept_header!('API vendor or version not found.')
47
- end
48
-
49
- def q_values_mime_types
50
- @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
51
- end
52
-
53
- def version_and_vendor?
54
- q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
55
- end
56
-
57
- def invalid_accept_header!(message)
58
- raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
59
- end
60
-
61
- def invalid_version_header!(message)
62
- raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
63
- end
64
-
65
- def fail!(grape_allowed_methods)
66
- return grape_allowed_methods if grape_allowed_methods.present?
67
-
68
- media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
69
- vendor_not_found!(media_types) || version_not_found!(media_types)
70
- end
71
-
72
- def vendor_not_found!(media_types)
73
- return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }
74
-
75
- invalid_accept_header!('API vendor not found.')
76
- end
77
-
78
- def version_not_found!(media_types)
79
- return unless media_types.all? { |media_type| media_type&.version && versions.exclude?(media_type.version) }
80
-
81
- invalid_version_header!('API version not found.')
82
- end
83
-
84
- def error_headers
85
- cascade ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
86
- end
87
-
88
- def available_media_types(content_types)
89
- [].tap do |available_media_types|
90
- base_media_type = "application/vnd.#{vendor}"
91
- content_types.each_key do |extension|
92
- versions&.reverse_each do |version|
93
- available_media_types << "#{base_media_type}-#{version}+#{extension}"
94
- available_media_types << "#{base_media_type}-#{version}"
95
- end
96
- available_media_types << "#{base_media_type}+#{extension}"
97
- end
98
-
99
- available_media_types << base_media_type
100
- available_media_types.concat(content_types.values.flatten)
101
- end
102
- end
103
- end
104
- end
105
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grape
4
- module Util
5
- module Registrable
6
- def default_elements
7
- @default_elements ||= {}
8
- end
9
-
10
- def register(format, element)
11
- default_elements[format] = element unless default_elements[format]
12
- end
13
- end
14
- end
15
- end