grape 1.5.0 → 1.6.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -2
  3. data/README.md +43 -10
  4. data/UPGRADING.md +91 -0
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +13 -17
  7. data/lib/grape/api.rb +7 -14
  8. data/lib/grape/cookies.rb +2 -0
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/desc.rb +3 -5
  11. data/lib/grape/dsl/helpers.rb +6 -4
  12. data/lib/grape/dsl/inside_route.rb +18 -9
  13. data/lib/grape/dsl/middleware.rb +4 -4
  14. data/lib/grape/dsl/parameters.rb +11 -7
  15. data/lib/grape/dsl/request_response.rb +9 -6
  16. data/lib/grape/dsl/routing.rb +7 -6
  17. data/lib/grape/dsl/settings.rb +5 -5
  18. data/lib/grape/endpoint.rb +21 -36
  19. data/lib/grape/error_formatter/json.rb +2 -6
  20. data/lib/grape/error_formatter/xml.rb +2 -6
  21. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  22. data/lib/grape/exceptions/validation.rb +2 -3
  23. data/lib/grape/exceptions/validation_errors.rb +1 -1
  24. data/lib/grape/formatter/json.rb +1 -0
  25. data/lib/grape/formatter/serializable_hash.rb +2 -1
  26. data/lib/grape/formatter/xml.rb +1 -0
  27. data/lib/grape/locale/en.yml +1 -1
  28. data/lib/grape/middleware/auth/base.rb +3 -3
  29. data/lib/grape/middleware/base.rb +4 -2
  30. data/lib/grape/middleware/error.rb +1 -1
  31. data/lib/grape/middleware/formatter.rb +4 -4
  32. data/lib/grape/middleware/stack.rb +10 -16
  33. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -5
  34. data/lib/grape/middleware/versioner/header.rb +6 -4
  35. data/lib/grape/middleware/versioner/param.rb +1 -0
  36. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  37. data/lib/grape/middleware/versioner/path.rb +2 -0
  38. data/lib/grape/parser/json.rb +1 -1
  39. data/lib/grape/parser/xml.rb +1 -1
  40. data/lib/grape/path.rb +1 -0
  41. data/lib/grape/request.rb +3 -0
  42. data/lib/grape/router/attribute_translator.rb +1 -1
  43. data/lib/grape/router/pattern.rb +1 -1
  44. data/lib/grape/router/route.rb +2 -2
  45. data/lib/grape/router.rb +6 -0
  46. data/lib/grape/util/inheritable_setting.rb +1 -3
  47. data/lib/grape/util/lazy_value.rb +3 -2
  48. data/lib/grape/validations/attributes_iterator.rb +8 -0
  49. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  50. data/lib/grape/validations/params_scope.rb +92 -58
  51. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  52. data/lib/grape/validations/types/custom_type_coercer.rb +3 -2
  53. data/lib/grape/validations/types/dry_type_coercer.rb +1 -1
  54. data/lib/grape/validations/types/invalid_value.rb +24 -0
  55. data/lib/grape/validations/types/json.rb +2 -1
  56. data/lib/grape/validations/types/primitive_coercer.rb +3 -3
  57. data/lib/grape/validations/types.rb +1 -4
  58. data/lib/grape/validations/validator_factory.rb +1 -1
  59. data/lib/grape/validations/validators/all_or_none.rb +1 -0
  60. data/lib/grape/validations/validators/as.rb +4 -8
  61. data/lib/grape/validations/validators/at_least_one_of.rb +1 -0
  62. data/lib/grape/validations/validators/base.rb +12 -7
  63. data/lib/grape/validations/validators/coerce.rb +8 -9
  64. data/lib/grape/validations/validators/default.rb +1 -0
  65. data/lib/grape/validations/validators/exactly_one_of.rb +1 -0
  66. data/lib/grape/validations/validators/multiple_params_base.rb +5 -2
  67. data/lib/grape/validations/validators/mutual_exclusion.rb +1 -0
  68. data/lib/grape/validations/validators/presence.rb +1 -0
  69. data/lib/grape/validations/validators/regexp.rb +1 -0
  70. data/lib/grape/validations/validators/same_as.rb +1 -0
  71. data/lib/grape/validations/validators/values.rb +3 -0
  72. data/lib/grape/version.rb +1 -1
  73. data/lib/grape.rb +3 -1
  74. data/spec/grape/api/custom_validations_spec.rb +1 -0
  75. data/spec/grape/api/routes_with_requirements_spec.rb +8 -8
  76. data/spec/grape/api_remount_spec.rb +9 -4
  77. data/spec/grape/api_spec.rb +203 -37
  78. data/spec/grape/dsl/callbacks_spec.rb +1 -1
  79. data/spec/grape/dsl/middleware_spec.rb +1 -1
  80. data/spec/grape/dsl/parameters_spec.rb +1 -0
  81. data/spec/grape/dsl/routing_spec.rb +1 -1
  82. data/spec/grape/endpoint/declared_spec.rb +259 -1
  83. data/spec/grape/endpoint_spec.rb +18 -5
  84. data/spec/grape/entity_spec.rb +10 -10
  85. data/spec/grape/middleware/auth/dsl_spec.rb +1 -1
  86. data/spec/grape/middleware/error_spec.rb +1 -2
  87. data/spec/grape/middleware/formatter_spec.rb +2 -2
  88. data/spec/grape/middleware/stack_spec.rb +4 -3
  89. data/spec/grape/request_spec.rb +1 -1
  90. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  91. data/spec/grape/validations/params_scope_spec.rb +37 -3
  92. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  93. data/spec/grape/validations/types/primitive_coercer_spec.rb +2 -2
  94. data/spec/grape/validations/validators/coerce_spec.rb +129 -22
  95. data/spec/grape/validations/validators/except_values_spec.rb +2 -2
  96. data/spec/grape/validations/validators/values_spec.rb +15 -11
  97. data/spec/grape/validations_spec.rb +280 -0
  98. data/spec/shared/versioning_examples.rb +22 -22
  99. data/spec/spec_helper.rb +1 -1
  100. data/spec/support/basic_auth_encode_helpers.rb +1 -1
  101. data/spec/support/versioned_helpers.rb +1 -1
  102. metadata +8 -6
@@ -22,6 +22,7 @@ module Grape
22
22
 
23
23
  def after
24
24
  return unless @app_response
25
+
25
26
  status, headers, bodies = *@app_response
26
27
 
27
28
  if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
@@ -79,7 +80,7 @@ module Grape
79
80
  (request.post? || request.put? || request.patch? || request.delete?) &&
80
81
  (!request.form_data? || !request.media_type) &&
81
82
  !request.parseable_data? &&
82
- (request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
83
+ (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
83
84
 
84
85
  return unless (input = env[Grape::Env::RACK_INPUT])
85
86
 
@@ -96,9 +97,7 @@ module Grape
96
97
  def read_rack_input(body)
97
98
  fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
98
99
 
99
- unless content_type_for(fmt)
100
- throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported."
101
- end
100
+ throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
102
101
  parser = Grape::Parser.parser_for fmt, **options
103
102
  if parser
104
103
  begin
@@ -145,6 +144,7 @@ module Grape
145
144
  fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
146
145
  # avoid symbol memory leak on an unknown format
147
146
  return fmt.to_sym if content_type_for(fmt)
147
+
148
148
  fmt
149
149
  end
150
150
 
@@ -6,12 +6,11 @@ module Grape
6
6
  # It allows to insert and insert after
7
7
  class Stack
8
8
  class Middleware
9
- attr_reader :args, :opts, :block, :klass
9
+ attr_reader :args, :block, :klass
10
10
 
11
- def initialize(klass, *args, **opts, &block)
11
+ def initialize(klass, *args, &block)
12
12
  @klass = klass
13
- @args = args
14
- @opts = opts
13
+ @args = args
15
14
  @block = block
16
15
  end
17
16
 
@@ -32,16 +31,8 @@ module Grape
32
31
  klass.to_s
33
32
  end
34
33
 
35
- if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7')
36
- def use_in(builder)
37
- block ? builder.use(klass, *args, **opts, &block) : builder.use(klass, *args, **opts)
38
- end
39
- else
40
- def use_in(builder)
41
- args = self.args
42
- args += [opts] unless opts.empty?
43
- block ? builder.use(klass, *args, &block) : builder.use(klass, *args)
44
- end
34
+ def use_in(builder)
35
+ builder.use(@klass, *@args, &@block)
45
36
  end
46
37
  end
47
38
 
@@ -54,8 +45,8 @@ module Grape
54
45
  @others = []
55
46
  end
56
47
 
57
- def each
58
- @middlewares.each { |x| yield x }
48
+ def each(&block)
49
+ @middlewares.each(&block)
59
50
  end
60
51
 
61
52
  def size
@@ -75,6 +66,7 @@ module Grape
75
66
  middleware = self.class::Middleware.new(*args, &block)
76
67
  middlewares.insert(index, middleware)
77
68
  end
69
+ ruby2_keywords :insert if respond_to?(:ruby2_keywords, true)
78
70
 
79
71
  alias insert_before insert
80
72
 
@@ -82,11 +74,13 @@ module Grape
82
74
  index = assert_index(index, :after)
83
75
  insert(index + 1, *args, &block)
84
76
  end
77
+ ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
85
78
 
86
79
  def use(*args, &block)
87
80
  middleware = self.class::Middleware.new(*args, &block)
88
81
  middlewares.push(middleware)
89
82
  end
83
+ ruby2_keywords :use if respond_to?(:ruby2_keywords, true)
90
84
 
91
85
  def merge_with(middleware_specs)
92
86
  middleware_specs.each do |operation, *args|
@@ -22,11 +22,9 @@ module Grape
22
22
  def before
23
23
  potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
24
24
 
25
- if strict?
25
+ if strict? && potential_version.empty?
26
26
  # If no Accept-Version header:
27
- if potential_version.empty?
28
- throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
29
- end
27
+ throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
30
28
  end
31
29
 
32
30
  return if potential_version.empty?
@@ -51,7 +49,7 @@ module Grape
51
49
  # of routes (see Grape::Router) for more information). To prevent
52
50
  # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
53
51
  def cascade?
54
- if options[:version_options] && options[:version_options].key?(:cascade)
52
+ if options[:version_options]&.key?(:cascade)
55
53
  options[:version_options][:cascade]
56
54
  else
57
55
  true
@@ -26,10 +26,10 @@ module Grape
26
26
  # route.
27
27
  class Header < Base
28
28
  VENDOR_VERSION_HEADER_REGEX =
29
- /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze
29
+ /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze
30
30
 
31
- HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/.freeze
32
- HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/.freeze
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
33
 
34
34
  def before
35
35
  strict_header_checks if strict?
@@ -52,12 +52,14 @@ module Grape
52
52
 
53
53
  def strict_accept_header_presence_check
54
54
  return unless header.qvalues.empty?
55
+
55
56
  fail_with_invalid_accept_header!('Accept header must be set.')
56
57
  end
57
58
 
58
59
  def strict_version_vendor_accept_header_presence_check
59
60
  return unless versions.present?
60
61
  return if an_accept_header_with_version_and_vendor_is_present?
62
+
61
63
  fail_with_invalid_accept_header!('API vendor or version not found.')
62
64
  end
63
65
 
@@ -160,7 +162,7 @@ module Grape
160
162
  # information). To prevent # this behavior, and not add the `X-Cascade`
161
163
  # header, one can set the `:cascade` option to `false`.
162
164
  def cascade?
163
- if version_options && version_options.key?(:cascade)
165
+ if version_options&.key?(:cascade)
164
166
  version_options[:cascade]
165
167
  else
166
168
  true
@@ -32,6 +32,7 @@ module Grape
32
32
  def before
33
33
  potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey]
34
34
  return if potential_version.nil?
35
+
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 }
36
37
  env[Grape::Env::API_VERSION] = potential_version
37
38
  env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  module Rack
4
5
  module Accept
5
6
  module Header
6
- ALLOWED_CHARACTERS = %r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}.freeze
7
+ ALLOWED_CHARACTERS = %r{^([a-z*]+)/([a-z0-9*&\^\-_#{$ERROR_INFO}.+]+)(?:;([a-z0-9=;]+))?$}.freeze
7
8
  class << self
8
9
  # Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
9
10
  def parse_media_type(media_type)
@@ -37,6 +37,7 @@ module Grape
37
37
  pieces = path.split('/')
38
38
  potential_version = pieces[1]
39
39
  return unless potential_version&.match?(options[:pattern])
40
+
40
41
  throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
41
42
  env[Grape::Env::API_VERSION] = potential_version
42
43
  end
@@ -45,6 +46,7 @@ module Grape
45
46
 
46
47
  def mounted_path?(path)
47
48
  return false unless mount_path && path.start_with?(mount_path)
49
+
48
50
  rest = path.slice(mount_path.length..-1)
49
51
  rest.start_with?('/') || rest.empty?
50
52
  end
@@ -8,7 +8,7 @@ module Grape
8
8
  ::Grape::Json.load(object)
9
9
  rescue ::Grape::Json::ParseError
10
10
  # handle JSON parsing errors via the rescue handlers or provide error message
11
- raise Grape::Exceptions::InvalidMessageBody, 'application/json'
11
+ raise Grape::Exceptions::InvalidMessageBody.new('application/json')
12
12
  end
13
13
  end
14
14
  end
@@ -8,7 +8,7 @@ module Grape
8
8
  ::Grape::Xml.parse(object)
9
9
  rescue ::Grape::Xml::ParseError
10
10
  # handle XML parsing errors via the rescue handlers or provide error message
11
- raise Grape::Exceptions::InvalidMessageBody, 'application/xml'
11
+ raise Grape::Exceptions::InvalidMessageBody.new('application/xml')
12
12
  end
13
13
  end
14
14
  end
data/lib/grape/path.rb CHANGED
@@ -91,6 +91,7 @@ module Grape
91
91
 
92
92
  def split_setting(key)
93
93
  return if settings[key].nil?
94
+
94
95
  settings[key].to_s.split('/')
95
96
  end
96
97
  end
data/lib/grape/request.rb CHANGED
@@ -15,6 +15,8 @@ module Grape
15
15
 
16
16
  def params
17
17
  @params ||= build_params
18
+ rescue EOFError
19
+ raise Grape::Exceptions::EmptyMessageBody.new(content_type)
18
20
  end
19
21
 
20
22
  def headers
@@ -35,6 +37,7 @@ module Grape
35
37
  Grape::Util::LazyObject.new do
36
38
  env.each_pair.with_object({}) do |(k, v), headers|
37
39
  next unless k.to_s.start_with? HTTP_PREFIX
40
+
38
41
  transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k)
39
42
  headers[transformed_header] = v
40
43
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  class Router
5
5
  # this could be an OpenStruct, but doesn't work in Ruby 2.3.0, see https://bugs.ruby-lang.org/issues/12251
6
6
  class AttributeTranslator
7
- attr_reader :attributes, :request_method, :requirements
7
+ attr_reader :attributes
8
8
 
9
9
  ROUTE_ATTRIBUTES = %i[
10
10
  prefix
@@ -41,7 +41,7 @@ module Grape
41
41
  end
42
42
 
43
43
  pattern = -pattern.split('/').tap do |parts|
44
- parts[parts.length - 1] = '?' + parts.last
44
+ parts[parts.length - 1] = "?#{parts.last}"
45
45
  end.join('/') if pattern.end_with?('*path')
46
46
 
47
47
  PatternCache[[pattern, suffix]]
@@ -84,8 +84,8 @@ module Grape
84
84
  path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
85
85
  path = File.realpath(path) if Pathname.new(path).relative?
86
86
  expected ||= name
87
- warn <<-WARNING
88
- #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.
87
+ warn <<~WARNING
88
+ #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.
89
89
  WARNING
90
90
  end
91
91
  end
data/lib/grape/router.rb CHANGED
@@ -28,6 +28,7 @@ module Grape
28
28
 
29
29
  def compile!
30
30
  return if compiled
31
+
31
32
  @union = Regexp.union(@neutral_regexes)
32
33
  @neutral_regexes = nil
33
34
  self.class.supported_methods.each do |method|
@@ -60,6 +61,7 @@ module Grape
60
61
  def recognize_path(input)
61
62
  any = with_optimization { greedy_match?(input) }
62
63
  return if any == default_response
64
+
63
65
  any.endpoint
64
66
  end
65
67
 
@@ -80,6 +82,7 @@ module Grape
80
82
  map[method].each do |route|
81
83
  next if exact_route == route
82
84
  next unless route.match?(input)
85
+
83
86
  response = process_route(route, env)
84
87
  break unless cascade?(response)
85
88
  end
@@ -91,6 +94,7 @@ module Grape
91
94
  response = yield(input, method)
92
95
 
93
96
  return response if response && !(cascade = cascade?(response))
97
+
94
98
  last_neighbor_route = greedy_match?(input)
95
99
 
96
100
  # If last_neighbor_route exists and request method is OPTIONS,
@@ -139,12 +143,14 @@ module Grape
139
143
  def match?(input, method)
140
144
  current_regexp = @optimized_map[method]
141
145
  return unless current_regexp.match(input)
146
+
142
147
  last_match = Regexp.last_match
143
148
  @map[method].detect { |route| last_match["_#{route.index}"] }
144
149
  end
145
150
 
146
151
  def greedy_match?(input)
147
152
  return unless @union.match(input)
153
+
148
154
  last_match = Regexp.last_match
149
155
  @neutral_map.detect { |route| last_match["_#{route.index}"] }
150
156
  end
@@ -5,9 +5,7 @@ module Grape
5
5
  # A branchable, inheritable settings object which can store both stackable
6
6
  # and inheritable values (see InheritableValues and StackableValues).
7
7
  class InheritableSetting
8
- attr_accessor :route, :api_class, :namespace
9
- attr_accessor :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable
10
- attr_accessor :parent, :point_in_time_copies
8
+ attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent, :point_in_time_copies
11
9
 
12
10
  # Retrieve global settings.
13
11
  def self.global
@@ -49,9 +49,10 @@ module Grape
49
49
  end
50
50
 
51
51
  def []=(key, value)
52
- @value_hash[key] = if value.is_a?(Hash)
52
+ @value_hash[key] = case value
53
+ when Hash
53
54
  LazyValueHash.new(value)
54
- elsif value.is_a?(Array)
55
+ when Array
55
56
  LazyValueArray.new(value)
56
57
  else
57
58
  LazyValue.new(value)
@@ -48,6 +48,14 @@ module Grape
48
48
  def yield_attributes(_resource_params, _attrs)
49
49
  raise NotImplementedError
50
50
  end
51
+
52
+ # This is a special case so that we can ignore tree's where option
53
+ # values are missing lower down. Unfortunately we can remove this
54
+ # are the parameter parsing stage as they are required to ensure
55
+ # the correct indexing is maintained
56
+ def skip?(val)
57
+ val == Grape::DSL::Parameters::EmptyOptionalValue
58
+ end
51
59
  end
52
60
  end
53
61
  end
@@ -6,7 +6,7 @@ module Grape
6
6
  private
7
7
 
8
8
  def yield_attributes(resource_params, _attrs)
9
- yield resource_params
9
+ yield resource_params, skip?(resource_params)
10
10
  end
11
11
  end
12
12
  end