grape 3.2.1 → 3.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. data/lib/grape/middleware/globals.rb +0 -14
@@ -3,10 +3,11 @@
3
3
  module Grape
4
4
  module Util
5
5
  module Lazy
6
- class Value
6
+ class Value < Base
7
7
  attr_reader :access_keys
8
8
 
9
9
  def initialize(value, access_keys = [])
10
+ super()
10
11
  @value = value
11
12
  @access_keys = access_keys
12
13
  end
@@ -20,18 +21,10 @@ module Grape
20
21
  @value
21
22
  end
22
23
 
23
- def lazy?
24
- true
25
- end
26
-
27
24
  def reached_by(parent_access_keys, access_key)
28
25
  @access_keys = parent_access_keys + [access_key]
29
26
  self
30
27
  end
31
-
32
- def to_s
33
- evaluate.to_s
34
- end
35
28
  end
36
29
  end
37
30
  end
@@ -5,28 +5,25 @@ module Grape
5
5
  module Lazy
6
6
  class ValueEnumerable < Value
7
7
  def [](key)
8
- if @value_hash[key].nil?
9
- Value.new(nil).reached_by(access_keys, key)
10
- else
11
- @value_hash[key].reached_by(access_keys, key)
12
- end
8
+ return Value.new(nil).reached_by(access_keys, key) if @value_hash[key].nil?
9
+
10
+ @value_hash[key].reached_by(access_keys, key)
13
11
  end
14
12
 
15
13
  def fetch(access_keys)
16
- fetched_keys = access_keys.dup
17
- value = self[fetched_keys.shift]
18
- fetched_keys.any? ? value.fetch(fetched_keys) : value
14
+ access_keys.reduce(self) { |node, key| node[key] }
19
15
  end
20
16
 
21
17
  def []=(key, value)
22
- @value_hash[key] = case value
23
- when Hash
24
- ValueHash.new(value)
25
- when Array
26
- ValueArray.new(value)
27
- else
28
- Value.new(value)
29
- end
18
+ value_class = case value
19
+ when Hash
20
+ ValueHash
21
+ when Array
22
+ ValueArray
23
+ else
24
+ Value
25
+ end
26
+ @value_hash[key] = value_class.new(value)
30
27
  end
31
28
  end
32
29
  end
@@ -20,10 +20,6 @@ module Grape
20
20
  end
21
21
 
22
22
  def ==(other)
23
- eql?(other)
24
- end
25
-
26
- def eql?(other)
27
23
  self.class == other.class &&
28
24
  other.type == type &&
29
25
  other.subtype == subtype &&
@@ -31,6 +27,7 @@ module Grape
31
27
  other.version == version &&
32
28
  other.format == format
33
29
  end
30
+ alias eql? ==
34
31
 
35
32
  def hash
36
33
  [self.class, type, subtype, vendor, version, format].hash
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module PathNormalizer
6
+ # Taken from Rails
7
+ # call("/foo") # => "/foo"
8
+ # call("/foo/") # => "/foo"
9
+ # call("foo") # => "/foo"
10
+ # call("") # => "/"
11
+ # call("/%ab") # => "/%AB"
12
+ # https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19
13
+ def self.call(path)
14
+ return '/' unless path
15
+ return path if path == '/'
16
+
17
+ # Fast path for the overwhelming majority of paths that don't need to be normalized
18
+ return path if path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//}))
19
+
20
+ # Slow path
21
+ encoding = path.encoding
22
+ path = "/#{path}"
23
+ path.squeeze!('/')
24
+
25
+ unless path == '/'
26
+ path.delete_suffix!('/')
27
+ path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
28
+ end
29
+
30
+ path.force_encoding(encoding)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -20,7 +20,7 @@ module Grape
20
20
  end
21
21
 
22
22
  def registry
23
- @registry ||= {}.with_indifferent_access
23
+ @registry ||= ActiveSupport::HashWithIndifferentAccess.new
24
24
  end
25
25
  end
26
26
  end
@@ -3,24 +3,27 @@
3
3
  module Grape
4
4
  module Util
5
5
  class StackableValues < BaseInheritable
6
- # Even if there is no value, an empty array will be returned.
6
+ EMPTY = [].freeze
7
+
8
+ # Even if there is no value, an empty (frozen) array will be returned.
7
9
  def [](name)
8
- inherited_value = inherited_values[name]
9
- new_value = new_values[name]
10
+ inherited_value = @inherited_values[name]
11
+ new_value = @new_values && @new_values[name]
10
12
 
11
- return new_value || [] unless inherited_value
13
+ return new_value || EMPTY unless inherited_value
12
14
 
13
15
  concat_values(inherited_value, new_value)
14
16
  end
15
17
 
16
18
  def []=(name, value)
17
- new_values[name] ||= []
18
- new_values[name].push value
19
+ @new_values ||= {}
20
+ @new_values[name] ||= []
21
+ @new_values[name].push value
19
22
  end
20
23
 
21
24
  def to_hash
22
- keys.each_with_object({}) do |key, result|
23
- result[key] = self[key]
25
+ keys.to_h do |key|
26
+ [key, self[key]]
24
27
  end
25
28
  end
26
29
 
@@ -3,37 +3,37 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class AttributesIterator
6
- include Enumerable
7
-
8
- attr_reader :scope
9
-
10
- def initialize(attrs, scope, params)
6
+ # +attrs+ and +scope+ are static per validator; only +params+ varies
7
+ # per request, so an instance can be built once and reused (it keeps
8
+ # no request-derived state). Reused instances are shared across
9
+ # threads, so +each+ must stay free of mutable instance state.
10
+ def initialize(attrs, scope)
11
11
  @attrs = attrs
12
12
  @scope = scope
13
- @original_params = scope.params(params)
14
- @params = Array.wrap(@original_params)
15
13
  end
16
14
 
17
- def each(&)
18
- do_each(@params, &) # because we need recursion for nested arrays
15
+ def each(params, &)
16
+ original_params = @scope.params(params)
17
+ # because we need recursion for nested arrays
18
+ do_each(Array.wrap(original_params), original_params, &)
19
19
  end
20
20
 
21
21
  private
22
22
 
23
- def do_each(params_to_process, parent_indices = [], &block)
23
+ def do_each(params_to_process, original_params, parent_indices = [], &block)
24
24
  params_to_process.each_with_index do |resource_params, index|
25
25
  # when we get arrays of arrays it means that target element located inside array
26
26
  # we need this because we want to know parent arrays indices
27
27
  if resource_params.is_a?(Array)
28
- do_each(resource_params, [index] + parent_indices, &block)
28
+ do_each(resource_params, original_params, [index] + parent_indices, &block)
29
29
  next
30
30
  end
31
31
 
32
32
  if @scope.type == Array
33
- next unless @original_params.is_a?(Array) # do not validate content of array if it isn't array
33
+ next unless original_params.is_a?(Array) # do not validate content of array if it isn't array
34
34
 
35
35
  store_indices(@scope, index, parent_indices)
36
- elsif @original_params.is_a?(Array)
36
+ elsif original_params.is_a?(Array)
37
37
  # Lateral scope (no @element) whose params resolved to an array —
38
38
  # delegate index tracking to the nearest array-typed ancestor so
39
39
  # that full_name produces the correct bracketed index.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ # Immutable value object describing how a parameter is coerced. Assembled
6
+ # by {ValidationsSpec#coerce_options} from the parsed +type+/+coerce_with+/
7
+ # +coerce_message+ declaration — never written by the user — and consumed
8
+ # by {ParamsScope#check_coerce_with} / {ParamsScope#validate_coerce} and by
9
+ # {Validators::Validators::CoerceValidator} (which receives it as its
10
+ # +options+ argument).
11
+ #
12
+ # All three fields may be +nil+ (e.g. a remountable API evaluated on its
13
+ # base instance has no resolved +type+ yet).
14
+ # +coerce_method+ (not +method+) avoids shadowing +Object#method+.
15
+ CoerceOptions = Data.define(:type, :coerce_method, :message) do
16
+ def initialize(type: nil, coerce_method: nil, message: nil)
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ # Stand-in for the +@api+ object that {ParamsScope} normally writes to.
6
+ # Used when evaluating a single variant block of a +oneof:+ schema so that
7
+ # the variant's validators are captured locally rather than registered on
8
+ # the real API. Exposes only the slice of the API surface that
9
+ # ParamsScope and its helpers touch during definition.
10
+ class OneofCollector
11
+ attr_reader :inheritable_setting
12
+
13
+ def initialize
14
+ @inheritable_setting = Grape::Util::InheritableSetting.new
15
+ @inheritable_setting.namespace_inheritable[:do_not_document] = true
16
+ end
17
+
18
+ def configuration
19
+ nil
20
+ end
21
+
22
+ def validators
23
+ inheritable_setting.namespace_stackable[:validations]
24
+ end
25
+
26
+ def declared_params
27
+ inheritable_setting.namespace_stackable[:declared_params]
28
+ end
29
+
30
+ # Evaluate +variant_block+ in a fresh +ParamsScope+ backed by a new
31
+ # collector and return the validators that the block registered.
32
+ def self.collect(variant_block)
33
+ collector = new
34
+ ParamsScope.new(api: collector, type: Hash, &variant_block)
35
+ collector.validators
36
+ end
37
+ end
38
+ end
39
+ end
@@ -29,28 +29,33 @@ module Grape
29
29
  Fiber[FIBER_KEY]
30
30
  end
31
31
 
32
- def initialize
33
- @index_tracker = {}.compare_by_identity
34
- @qualifying_params_tracker = {}.compare_by_identity
35
- end
36
-
37
32
  def store_index(scope, index)
38
- @index_tracker.store(scope, index)
33
+ index_tracker.store(scope, index)
39
34
  end
40
35
 
41
36
  def index_for(scope)
42
- @index_tracker[scope]
37
+ index_tracker[scope]
43
38
  end
44
39
 
45
40
  # Returns qualifying params for +scope+, or EMPTY_PARAMS if none were stored.
46
41
  # Note: an explicitly stored empty array and "never stored" are treated identically
47
42
  # by callers (both yield a blank result that falls through to the parent params).
48
43
  def qualifying_params(scope)
49
- @qualifying_params_tracker.fetch(scope, EMPTY_PARAMS)
44
+ qualifying_params_tracker.fetch(scope, EMPTY_PARAMS)
50
45
  end
51
46
 
52
47
  def store_qualifying_params(scope, params)
53
- @qualifying_params_tracker.store(scope, params)
48
+ qualifying_params_tracker.store(scope, params)
49
+ end
50
+
51
+ private
52
+
53
+ def index_tracker
54
+ @index_tracker ||= {}.compare_by_identity
55
+ end
56
+
57
+ def qualifying_params_tracker
58
+ @qualifying_params_tracker ||= {}.compare_by_identity
54
59
  end
55
60
  end
56
61
  end
@@ -2,39 +2,41 @@
2
2
 
3
3
  module Grape
4
4
  module Validations
5
- # Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an
6
- # internal API), the class only cleans up attributes to avoid junk in RAM.
7
-
5
+ # Documents parameters of an endpoint. Reads from a frozen
6
+ # +ValidationsSpec+; never mutates the user's validations hash.
8
7
  module ParamsDocumentation
9
- def document_params(attrs, validations, type = nil, values = nil, except_values = nil)
10
- return validations.except!(:desc, :description, :documentation) if @api.inheritable_setting.namespace_inheritable[:do_not_document]
8
+ def document_params(attrs, spec)
9
+ return if @api.inheritable_setting.namespace_inheritable[:do_not_document]
11
10
 
12
- documented_attrs = attrs.each_with_object({}) do |name, memo|
13
- memo[full_name(name)] = extract_details(validations, type, values, except_values)
11
+ documented_attrs = attrs.to_h do |name|
12
+ [full_name(name), extract_details(spec)]
14
13
  end
15
14
  @api.inheritable_setting.namespace_stackable[:params] = documented_attrs
16
15
  end
17
16
 
18
17
  private
19
18
 
20
- def extract_details(validations, type, values, except_values)
21
- {}.tap do |details|
22
- details[:required] = validations.key?(:presence)
23
- details[:type] = TypeCache[type] if type
24
- details[:values] = values if values
25
- details[:except_values] = except_values if except_values
26
- details[:default] = validations[:default] if validations.key?(:default)
27
- if validations.key?(:length)
28
- details[:min_length] = validations[:length][:min] if validations[:length].key?(:min)
29
- details[:max_length] = validations[:length][:max] if validations[:length].key?(:max)
30
- end
19
+ def extract_details(spec)
20
+ details = {}
21
+ details[:required] = spec.required?
22
+ details[:type] = TypeCache[spec.coerce_type] if spec.coerce_type
23
+ details[:values] = spec.values if spec.values
24
+ details[:except_values] = spec.except_values if spec.except_values
25
+ details[:default] = spec.default unless spec.default.nil?
26
+
27
+ length = spec.raw[:length]
28
+ if length.is_a?(Hash)
29
+ details[:min_length] = length[:min] if length.key?(:min)
30
+ details[:max_length] = length[:max] if length.key?(:max)
31
+ end
31
32
 
32
- desc = validations.delete(:desc) || validations.delete(:description)
33
- details[:desc] = desc if desc
33
+ desc = spec.raw[:desc] || spec.raw[:description]
34
+ details[:desc] = desc if desc
34
35
 
35
- documentation = validations.delete(:documentation)
36
- details[:documentation] = documentation if documentation
37
- end
36
+ documentation = spec.raw[:documentation]
37
+ details[:documentation] = documentation if documentation
38
+
39
+ details
38
40
  end
39
41
 
40
42
  class TypeCache < Grape::Util::Cache