grape 1.5.0 → 1.6.0

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