grape 4.0.0 → 4.1.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -1
  3. data/README.md +195 -95
  4. data/UPGRADING.md +364 -0
  5. data/grape.gemspec +1 -1
  6. data/lib/grape/api/instance.rb +57 -30
  7. data/lib/grape/api.rb +52 -18
  8. data/lib/grape/content_types.rb +26 -9
  9. data/lib/grape/cookies.rb +1 -1
  10. data/lib/grape/declared_params_handler.rb +35 -7
  11. data/lib/grape/dsl/declared.rb +5 -4
  12. data/lib/grape/dsl/desc.rb +1 -2
  13. data/lib/grape/dsl/entity.rb +35 -19
  14. data/lib/grape/dsl/headers.rb +3 -3
  15. data/lib/grape/dsl/helpers.rb +5 -8
  16. data/lib/grape/dsl/inside_route.rb +33 -27
  17. data/lib/grape/dsl/logger.rb +10 -4
  18. data/lib/grape/dsl/middleware.rb +4 -9
  19. data/lib/grape/dsl/mounting.rb +149 -0
  20. data/lib/grape/dsl/parameters.rb +78 -104
  21. data/lib/grape/dsl/request_response.rb +35 -22
  22. data/lib/grape/dsl/routing.rb +17 -191
  23. data/lib/grape/dsl/settings.rb +1 -1
  24. data/lib/grape/dsl/version_options.rb +2 -2
  25. data/lib/grape/dsl/versioning.rb +98 -0
  26. data/lib/grape/endpoint/state.rb +15 -0
  27. data/lib/grape/endpoint.rb +229 -137
  28. data/lib/grape/error_formatter/base.rb +28 -13
  29. data/lib/grape/error_formatter/json.rb +3 -0
  30. data/lib/grape/error_formatter/jsonl.rb +15 -0
  31. data/lib/grape/error_formatter/xml.rb +3 -1
  32. data/lib/grape/exceptions/error_response.rb +23 -7
  33. data/lib/grape/exceptions/halt.rb +39 -0
  34. data/lib/grape/exceptions/validation.rb +9 -9
  35. data/lib/grape/exceptions/validation_errors.rb +12 -2
  36. data/lib/grape/formatter/jsonl.rb +34 -0
  37. data/lib/grape/formatter/markdown.rb +17 -0
  38. data/lib/grape/formatter/md.rb +11 -0
  39. data/lib/grape/locale/en.yml +1 -0
  40. data/lib/grape/middleware/auth/base.rb +16 -1
  41. data/lib/grape/middleware/auth/strategy_info.rb +2 -2
  42. data/lib/grape/middleware/base.rb +19 -13
  43. data/lib/grape/middleware/error.rb +133 -35
  44. data/lib/grape/middleware/formatter.rb +235 -94
  45. data/lib/grape/middleware/precomputed_content_types.rb +14 -2
  46. data/lib/grape/middleware/stack.rb +7 -8
  47. data/lib/grape/middleware/versioner/base.rb +26 -10
  48. data/lib/grape/middleware/versioner/header.rb +70 -15
  49. data/lib/grape/middleware/versioner/param.rb +3 -5
  50. data/lib/grape/middleware/versioner/path.rb +20 -11
  51. data/lib/grape/middleware/versioner.rb +1 -1
  52. data/lib/grape/mountable.rb +2 -4
  53. data/lib/grape/namespace.rb +2 -3
  54. data/lib/grape/parser/jsonl.rb +19 -0
  55. data/lib/grape/request.rb +46 -8
  56. data/lib/grape/router/base_route.rb +41 -0
  57. data/lib/grape/router/greedy_route.rb +1 -1
  58. data/lib/grape/router/mustermann_pattern.rb +1 -1
  59. data/lib/grape/router/pattern/path.rb +1 -1
  60. data/lib/grape/router/pattern.rb +11 -5
  61. data/lib/grape/router/route.rb +42 -11
  62. data/lib/grape/router/route_buckets.rb +246 -0
  63. data/lib/grape/router.rb +262 -32
  64. data/lib/grape/util/endpoint_configuration.rb +7 -1
  65. data/lib/grape/util/inheritable_setting.rb +80 -161
  66. data/lib/grape/util/lazy/value.rb +52 -11
  67. data/lib/grape/util/media_type.rb +38 -15
  68. data/lib/grape/util/path_normalizer.rb +7 -2
  69. data/lib/grape/util/registry.rb +9 -2
  70. data/lib/grape/util/scrub.rb +33 -0
  71. data/lib/grape/util/translation.rb +6 -6
  72. data/lib/grape/validations/attributes_iterator.rb +73 -37
  73. data/lib/grape/validations/coerce_options.rb +1 -1
  74. data/lib/grape/validations/params_documentation.rb +15 -7
  75. data/lib/grape/validations/params_scope.rb +176 -43
  76. data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
  77. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  78. data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
  79. data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
  80. data/lib/grape/validations/types/primitive_coercer.rb +17 -0
  81. data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
  82. data/lib/grape/validations/types.rb +12 -18
  83. data/lib/grape/validations/validations_spec.rb +53 -22
  84. data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
  85. data/lib/grape/validations/validators/as_validator.rb +2 -3
  86. data/lib/grape/validations/validators/base.rb +124 -25
  87. data/lib/grape/validations/validators/coerce_validator.rb +2 -3
  88. data/lib/grape/validations/validators/default_validator.rb +18 -2
  89. data/lib/grape/validations/validators/except_values_validator.rb +16 -3
  90. data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
  91. data/lib/grape/validations/validators/length_validator.rb +24 -16
  92. data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
  93. data/lib/grape/validations/validators/values_validator.rb +18 -24
  94. data/lib/grape/version.rb +1 -2
  95. data/lib/grape.rb +3 -0
  96. metadata +18 -11
  97. data/lib/grape/util/lazy/value_array.rb +0 -21
  98. data/lib/grape/util/lazy/value_enumerable.rb +0 -31
  99. data/lib/grape/util/lazy/value_hash.rb +0 -21
  100. data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
  101. data/lib/grape/validations/single_attribute_iterator.rb +0 -28
@@ -14,17 +14,13 @@ module Grape
14
14
  # Returns a collection coercer which corresponds to a given type.
15
15
  # Example:
16
16
  #
17
- # collection_coercer_for(Array)
17
+ # collection_coercer_for([Integer])
18
18
  # #=> Grape::Validations::Types::ArrayCoercer
19
19
  def collection_coercer_for(type)
20
- case type
21
- when Array
22
- ArrayCoercer
23
- when Set
24
- SetCoercer
25
- else
26
- raise ArgumentError, "unknown type: `#{type}`"
27
- end
20
+ return ArrayCoercer if type.is_a?(Array)
21
+ return SetCoercer if type.is_a?(Set)
22
+
23
+ raise ArgumentError, "unknown type: `#{type}`"
28
24
  end
29
25
 
30
26
  # Returns an instance of a coercer for a given type
@@ -43,13 +39,25 @@ module Grape
43
39
  # Coerces the given value to a type which was specified during
44
40
  # initialization as a type argument.
45
41
  #
42
+ # Given a block, dry-types reports a value it cannot coerce by calling
43
+ # the block instead of raising. Raising is what cost: its CoercionError
44
+ # is re-raised with the backtrace of the error underneath, and building
45
+ # that backtrace as strings at request depth took about 25 µs for every
46
+ # rejected value -- every `types: [Integer, String]` param given a
47
+ # string, every 400 for a mistyped value.
48
+ #
49
+ # Every coercion dry-types runs takes that block, so none of them
50
+ # raises a CoercionError here. Anything that raises something else --
51
+ # +Kernel#String+, which +Coercible::String+ is built from, ignores the
52
+ # block and raises TypeError -- is answered by
53
+ # +CoerceValidator#coerce_value+, which rescues StandardError with the
54
+ # same InvalidValue.
55
+ #
46
56
  # @param val [Object]
47
57
  def call(val)
48
58
  return if val.nil?
49
59
 
50
- @coercer[val]
51
- rescue Dry::Types::CoercionError
52
- InvalidValue.new
60
+ @coercer.call(val) { InvalidValue.new }
53
61
  end
54
62
 
55
63
  protected
@@ -35,8 +35,7 @@ module Grape
35
35
 
36
36
  # Coerces the given value.
37
37
  #
38
- # @param val [String] value to be coerced, in grape
39
- # this should always be a string.
38
+ # @param val [Object] value to be coerced
40
39
  # @return [Object,InvalidValue] the coerced result, or an instance
41
40
  # of {InvalidValue} if the value could not be coerced.
42
41
  def call(val)
@@ -19,9 +19,16 @@ module Grape
19
19
  @coercer = cache_coercer[type]
20
20
  @rejected_inputs = REJECTED_INPUTS[type]
21
21
  @treat_empty_as_nil = type != String
22
+ @integer = type == Integer
22
23
  end
23
24
 
24
25
  def call(val)
26
+ # A value already of the declared type is what the dry-types coercer
27
+ # hands back for it -- the very same object, in both the params and
28
+ # the strict cache -- so the round trip through it only costs time.
29
+ # +instance_of?+ rather than +is_a?+: a subclass is not covered by
30
+ # that, and Coercible::String does rebuild one.
31
+ return val if val.instance_of?(@type)
25
32
  return InvalidValue.new if reject?(val)
26
33
  return if val.nil? || treat_as_nil?(val)
27
34
 
@@ -37,11 +44,21 @@ module Grape
37
44
  # but Virtus wouldn't accept it. So, this method only exists to not introduce
38
45
  # breaking changes.
39
46
  def reject?(val)
47
+ return fractional?(val) if @integer
40
48
  return false unless @rejected_inputs
41
49
 
42
50
  @rejected_inputs.any? { |klass| val.is_a?(klass) }
43
51
  end
44
52
 
53
+ # dry-types turns a number into an Integer with Kernel#Integer, which
54
+ # truncates, so a JSON body's 2.99 became 2 while the same value in a
55
+ # query string, "2.99", was rejected. A number with a fractional part
56
+ # is refused instead; a whole one such as 1.0 still becomes 1. Complex
57
+ # numbers have no fractional part to test and are left to dry-types.
58
+ def fractional?(val)
59
+ val.is_a?(Numeric) && val.real? && !(val % 1).zero?
60
+ end
61
+
45
62
  # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
46
63
  # absence of a value and coerces it into nil. See a discussion there
47
64
  # https://github.com/ruby-grape/grape/pull/2045
@@ -41,23 +41,37 @@ module Grape
41
41
 
42
42
  # Coerce the given value.
43
43
  #
44
+ # A value that is not an Array, and an Array holding a member none of
45
+ # the types accepts, are invalid, as they are for a collection of one
46
+ # type. A coercion method is handed the value whatever it is, as it is
47
+ # for +types:+, so it can build the collection out of a String.
48
+ #
44
49
  # @param value [Array<String>] collection of values to be coerced
45
- # @return [Array<Object>,Set<Object>,InvalidValue]
46
- # the coerced result, or an instance
47
- # of {InvalidValue} if the value could not be coerced.
50
+ # @return [Array<Object>,Set<Object>,InvalidValue,nil]
51
+ # the coerced result, nil when the value is nil or an empty String,
52
+ # or an instance of {InvalidValue} if the value could not be coerced.
48
53
  def call(value)
49
- return unless value.is_a? Array
54
+ return if value.nil? || (value.is_a?(String) && value.empty?)
50
55
 
51
- coerced =
52
- if @method
53
- @method.call(value)
54
- else
55
- value.map { |v| @member_coercer.call(v) }
56
- end
56
+ coerced = @method ? @method.call(value) : coerce_members(value)
57
+ return coerced if coerced.is_a?(InvalidValue)
57
58
  return Set.new coerced if @types.is_a? Set
58
59
 
59
60
  coerced
60
61
  end
62
+
63
+ private
64
+
65
+ def coerce_members(value)
66
+ return InvalidValue.new unless value.is_a?(Array)
67
+
68
+ value.map do |member|
69
+ coerced = @member_coercer.call(member)
70
+ return coerced if coerced.is_a?(InvalidValue)
71
+
72
+ coerced
73
+ end
74
+ end
61
75
  end
62
76
  end
63
77
  end
@@ -9,8 +9,8 @@ module Grape
9
9
  # Grape uses a number of tests and assertions to
10
10
  # work out exactly how a parameter should be handled,
11
11
  # based on the +type+ and +coerce_with+ options that
12
- # may be supplied to {Grape::Dsl::Parameters#requires}
13
- # and {Grape::Dsl::Parameters#optional}. The main
12
+ # may be supplied to {Grape::DSL::Parameters#requires}
13
+ # and {Grape::DSL::Parameters#optional}. The main
14
14
  # entry point for this process is {Types.build_coercer}.
15
15
  module Types
16
16
  module_function
@@ -112,11 +112,10 @@ module Grape
112
112
  type.respond_to?(:parse) && type.method(:parse).arity == 1
113
113
  end
114
114
 
115
- # Is the declared type an +Array+ or +Set+ of a {#custom?} type?
115
+ # Is the declared type an +Array+ or +Set+ of a {#custom?} or {#special?} type?
116
116
  #
117
117
  # @param type [Array<Class>,Class] type to check
118
- # @return [Boolean] true if +type+ is a collection of a type that implements
119
- # its own +#parse+ method.
118
+ # @return [Boolean] true if +type+ is a one-element collection of such a type
120
119
  def collection_of_custom?(type)
121
120
  array_or_set?(type) && type.length == 1 && (custom?(type.first) || special?(type.first))
122
121
  end
@@ -131,26 +130,21 @@ module Grape
131
130
  #
132
131
  # There are a few very special coercers which might be returned.
133
132
  #
134
- # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when
135
- # the given type implies values in an array with different types.
136
- # For example, +[Integer, String]+ allows integer and string values in
137
- # an array.
133
+ # +MultipleTypeCoercer+ is returned for a value that may be one of
134
+ # several types, e.g. +types: [Integer, String]+.
138
135
  #
139
- # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when
140
- # a method is specified by a user with +coerce_with+ option or the user
141
- # specifies a custom type which implements requirments of
142
- # +Grape::Types::CustomTypeCoercer+.
136
+ # +CustomTypeCoercer+ is returned for a +coerce_with+ method or a type
137
+ # with its own +parse+ (see {#custom?}).
143
138
  #
144
- # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the
145
- # previous one, but it expects an array or set of values having a custom
146
- # type implemented by the user.
139
+ # +CustomTypeCollectionCoercer+ is returned for an array or set of such
140
+ # a type (see {#collection_of_custom?}).
147
141
  #
148
142
  # There is also a group of custom types implemented by Grape, check
149
143
  # +Grape::Validations::Types::SPECIAL+ to get the full list.
150
144
  #
151
- # @param type [Class] the type to which input strings
152
- # should be coerced
145
+ # @param type [Class] the type to which input values should be coerced
153
146
  # @param method [Class,#call] the coercion method to use
147
+ # @param strict [Boolean] check the type rather than coerce
154
148
  # @return [Object] object to be used
155
149
  # for coercion and type validation
156
150
  def build_coercer(type, method: nil, strict: false)
@@ -9,9 +9,8 @@ module Grape
9
9
  #
10
10
  # Splits the raw entries into three logical buckets:
11
11
  #
12
- # * Spec-consumed keys (type/types/coerce*, presence/message,
13
- # default/fail_fast, doc keys) — exposed via named accessors and never
14
- # handed to validator dispatch.
12
+ # * Spec-consumed keys (type/types/coerce*, presence/message, fail_fast,
13
+ # doc keys) — never handed to validator dispatch.
15
14
  # * Shared opts (allow_blank, fail_fast) — read by every validator at
16
15
  # construction time via {#shared_opts}.
17
16
  # * Validator entries (everything else, e.g. +regexp+, +length+, +values+,
@@ -33,6 +32,9 @@ module Grape
33
32
  desc description documentation
34
33
  ].freeze
35
34
 
35
+ BOOLEAN_VALUES = [true, false].freeze
36
+ private_constant :BOOLEAN_VALUES
37
+
36
38
  attr_reader :raw, :coerce_type, :coerce_method, :coerce_message, :presence_options, :values, :except_values, :default, :allow_blank, :fail_fast, :shared_opts, :validator_entries
37
39
 
38
40
  def self.from(validations)
@@ -44,15 +46,11 @@ module Grape
44
46
 
45
47
  @raw = raw
46
48
  @coerce_type, @coerce_message, @coerce_method = parse_coerce(raw)
47
- @values = resolve_value(raw[:values])
48
- @except_values = resolve_value(raw[:except_values])
49
- @default = raw[:default]
49
+ read_values(raw)
50
50
  @presence_options = raw[:presence]
51
- @allow_blank = resolve_value(raw[:allow_blank])
52
- @fail_fast = raw[:fail_fast] || false
51
+ read_shared_opts(raw)
53
52
 
54
53
  @guessed_coerce_type = guess_coerce_type(declared_coerce_type, @values, @except_values)
55
- @shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
56
54
  @validator_entries = build_validator_entries(raw)
57
55
 
58
56
  validate!
@@ -70,6 +68,20 @@ module Grape
70
68
 
71
69
  private
72
70
 
71
+ # The values the declaration accepts, rejects and defaults to.
72
+ def read_values(raw)
73
+ @values = resolve_value(raw[:values])
74
+ @except_values = resolve_value(raw[:except_values])
75
+ @default = raw[:default]
76
+ end
77
+
78
+ # What every validator of the declaration reads (see #shared_opts).
79
+ def read_shared_opts(raw)
80
+ @allow_blank = resolve_value(raw[:allow_blank])
81
+ @fail_fast = raw[:fail_fast] || false
82
+ @shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
83
+ end
84
+
73
85
  # Cross-field consistency checks on the parsed declaration. Run at
74
86
  # construction so an incoherent spec (e.g. a +default+ outside +values+,
75
87
  # or +values+ whose elements don't match +type+) can never exist —
@@ -87,11 +99,25 @@ module Grape
87
99
  def check_incompatible_option_values(default, values, except_values)
88
100
  return if default.nil? || default.is_a?(Proc)
89
101
 
90
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && Array(default).any? { |def_val| !values.include?(def_val) }
102
+ defaults = Array(default)
103
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if any_outside?(defaults, values)
104
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) if any_among?(defaults, except_values)
105
+ end
106
+
107
+ # Whether one of +defaults+ is outside +values+, known now (see #fixed_values?).
108
+ def any_outside?(defaults, values)
109
+ fixed_values?(values) && defaults.any? { |default| !values.include?(default) }
110
+ end
91
111
 
92
- return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
112
+ # Whether one of +defaults+ is among +values+, known now (see #fixed_values?).
113
+ def any_among?(defaults, values)
114
+ fixed_values?(values) && defaults.any? { |default| values.include?(default) }
115
+ end
93
116
 
94
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
117
+ # A +values+ or +except_values+ list known at definition time: given,
118
+ # and not a Proc called on every request.
119
+ def fixed_values?(values)
120
+ values && !values.is_a?(Proc)
95
121
  end
96
122
 
97
123
  def validate_value_coercion(coerce_type, *values_list)
@@ -99,14 +125,19 @@ module Grape
99
125
 
100
126
  element_type = coerce_type.is_a?(Enumerable) ? coerce_type.first : coerce_type
101
127
  values_list.each do |values|
102
- next if !values || values.is_a?(Proc)
128
+ next unless fixed_values?(values)
103
129
 
104
130
  value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
105
- value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if element_type == Grape::API::Boolean
106
- raise Grape::Exceptions::IncompatibleOptionValues.new(:type, element_type, :values, values) unless value_types.all?(element_type)
131
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:type, element_type, :values, values) unless all_of_type?(value_types, element_type)
107
132
  end
108
133
  end
109
134
 
135
+ def all_of_type?(values, type)
136
+ return values.all? { |value| BOOLEAN_VALUES.include?(value) } if type == Grape::API::Boolean
137
+
138
+ values.all?(type)
139
+ end
140
+
110
141
  def build_validator_entries(raw)
111
142
  raw.reject do |k, _|
112
143
  SPEC_CONSUMED_KEYS.include?(k) || ParamsScope::RESERVED_DOCUMENTATION_KEYWORDS.include?(k)
@@ -114,17 +145,15 @@ module Grape
114
145
  end
115
146
 
116
147
  def parse_coerce(raw)
148
+ coerce_with = raw[:coerce_with]
117
149
  if raw.key?(:type)
118
150
  coerce, coerce_message = extract_value_and_message(raw[:type])
119
- coerce_with = raw[:coerce_with]
120
151
  return [Types::VariantCollectionCoercer.new(coerce, coerce_with), coerce_message, nil] if Types.multiple?(coerce)
121
152
  elsif raw.key?(:types)
122
153
  coerce, coerce_message = extract_value_and_message(raw[:types])
123
- coerce_with = raw[:coerce_with]
124
154
  else
125
- coerce = raw[:coerce]
126
- coerce_message = raw[:coerce_message]
127
- coerce_with = raw[:coerce_with]
155
+ coerce, message = extract_value_and_message(raw[:coerce])
156
+ coerce_message = raw[:coerce_message] || message
128
157
  end
129
158
 
130
159
  [coerce, coerce_message, coerce_with]
@@ -154,8 +183,10 @@ module Grape
154
183
  return coerce_type unless coerce_type == Array
155
184
 
156
185
  values_list.each do |values|
157
- next if !values || values.is_a?(Proc)
158
- return values.first.class if values.is_a?(Range) || !values.empty?
186
+ next unless fixed_values?(values)
187
+ # A beginless Range has no first element to ask.
188
+ return (values.begin || values.end).class if values.is_a?(Range)
189
+ return values.first.class unless values.empty?
159
190
  end
160
191
  coerce_type
161
192
  end
@@ -14,8 +14,8 @@ module Grape
14
14
  def validate_param!(attr_name, params)
15
15
  return if @value || !hash_like?(params)
16
16
 
17
- value = scrub(params[attr_name])
18
- return if value == false || value.present?
17
+ value = params[attr_name]
18
+ return if value == false || !Grape::Util::Scrub.blank?(value)
19
19
 
20
20
  validation_error!(attr_name)
21
21
  end
@@ -4,9 +4,8 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class AsValidator < Base
7
- # We use a validator for renaming parameters. This is just a marker for
8
- # the parameter scope to handle the renaming. No actual validation
9
- # happens here.
7
+ # Never dispatched: +as:+ renaming is handled by ParamsScope. No
8
+ # validation happens here.
10
9
  def validate_param!(*); end
11
10
  end
12
11
  end
@@ -7,10 +7,10 @@ module Grape
7
7
  #
8
8
  # == Freeze contract
9
9
  # Validator instances are shared across requests and are frozen after
10
- # initialization (via +.new+). All inputs (+options+, +opts+, +attrs+)
11
- # arrive pre-frozen from the DSL boundary, so subclass ivars derived
12
- # from them are frozen by construction. Lazy ivar assignment
13
- # (e.g. +memoize+, <tt>||=</tt>) will raise +FrozenError+ at request time.
10
+ # initialization (via +.new+). #initialize freezes +attrs+ and
11
+ # deep-freezes +options+, so subclass ivars derived from them are frozen
12
+ # by construction. Lazy ivar assignment (e.g. +memoize+, <tt>||=</tt>)
13
+ # will raise +FrozenError+ at request time.
14
14
  class Base
15
15
  extend Forwardable
16
16
  extend Grape::Util::FreezeOnNew
@@ -68,7 +68,8 @@ module Grape
68
68
  @scope = scope
69
69
  @opts = SharedOptions.new(**opts.slice(:allow_blank, :fail_fast))
70
70
  @exception_message = message(self.class.default_message_key) if self.class.default_message_key
71
- @iterator = iterator_class.new(@attrs, @scope).freeze
71
+ @iterator = AttributesIterator.new(@scope).freeze
72
+ choose_validation_path(scope)
72
73
  end
73
74
 
74
75
  # Validates a given request.
@@ -78,7 +79,7 @@ module Grape
78
79
  # @return [void]
79
80
  def validate(request)
80
81
  params = request.params
81
- return unless scope.should_validate?(params)
82
+ return unless @always_validated || scope.should_validate?(params)
82
83
 
83
84
  validate!(params)
84
85
  end
@@ -90,20 +91,12 @@ module Grape
90
91
  # @raise [Grape::Exceptions::Validation] if validation failed
91
92
  # @return [void]
92
93
  def validate!(params)
93
- # we collect errors inside array because
94
- # there may be more than one error per field
95
- array_errors = nil
94
+ return validate_elements!(scope.params(params)) if @direct_elements
96
95
 
97
- @iterator.each(params) do |val, attr_name, empty_val|
98
- next if !scope.required? && empty_val
99
- next unless scope.meets_dependency?(val, params)
96
+ scoped = scope.params(params) if @direct
97
+ return validate_attributes!(scoped) if scoped.is_a?(Hash)
100
98
 
101
- validate_param!(attr_name, val) if required? || (hash_like?(val) && val.key?(attr_name))
102
- rescue Grape::Exceptions::Validation => e
103
- (array_errors ||= []) << e
104
- end
105
-
106
- raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
99
+ validate_iterated!(params)
107
100
  end
108
101
 
109
102
  protected
@@ -123,10 +116,117 @@ module Grape
123
116
 
124
117
  alias required? required
125
118
 
126
- # The AttributesIterator subclass used to walk this validator's
127
- # attributes. Built once in #initialize and reused across requests.
128
- def iterator_class
129
- SingleAttributeIterator
119
+ # A scope's parent, optionality, dependency and type are all set
120
+ # before its block declares anything, so these are settled by the
121
+ # time a validator is built. See #validate and #validate!.
122
+ def choose_validation_path(scope)
123
+ @always_validated = scope.always_validated?
124
+ @direct = @always_validated && !scope.iterates_elements?
125
+ @direct_elements = scope.validated_when_given? && scope.iterates_elements? && scope.array_depth == 1
126
+ end
127
+
128
+ # #validate! on a scope that always validates and does not iterate
129
+ # elements, once its params resolved to a Hash: the root scope and the
130
+ # required Hash scopes under it, which is most validators of most
131
+ # endpoints. There the iterator hands back that Hash alone, and every
132
+ # scope on the chain is required with no dependency, so the checks
133
+ # around the attribute loop drop out. The machinery cost more than
134
+ # the validation itself.
135
+ def validate_attributes!(params)
136
+ array_errors = nil
137
+
138
+ @attrs.each do |attr_name|
139
+ validate_param!(attr_name, params) if required? || params.key?(attr_name)
140
+ rescue Grape::Exceptions::Validation => e
141
+ (array_errors ||= []) << e
142
+ end
143
+
144
+ raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
145
+ end
146
+
147
+ # #validate_attributes! for each element of an Array scope, such as
148
+ # +requires :items, type: Array do+ at the root, when it is the only
149
+ # scope on the chain that iterates elements: its params are then the
150
+ # request's Array as it came in, with no nesting for the iterator to
151
+ # descend into. It depends on no other param and every scope above it
152
+ # is always validated, so the iterator's per-element checks come down
153
+ # to the index it records for the error names and passing over the
154
+ # placeholder of an optional scope that was left out. An optional
155
+ # scope whose elements are all empty is passed over too, as the
156
+ # iterator does; otherwise an empty element was sent, and is validated
157
+ # like any other. An element that is not a Hash goes through the same
158
+ # +hash_like?+ test as on the iterator path, and the members of a scope
159
+ # that did not get an Array are left alone, as they are there: the
160
+ # scope's own type check reports it.
161
+ def validate_elements!(elements)
162
+ return unless elements.is_a?(Array)
163
+ return if passed_over_elements?(elements)
164
+
165
+ tracker = ParamScopeTracker.current
166
+ array_errors = nil
167
+
168
+ elements.each_with_index do |element, index|
169
+ tracker&.store_index(scope, index)
170
+ next if ParamsScope::EmptyOptionalValue.equal?(element)
171
+
172
+ array_errors = validate_element_attributes(element, array_errors)
173
+ end
174
+
175
+ raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
176
+ end
177
+
178
+ # #validate! on every other scope: each value the iterator yields is
179
+ # validated, unless the scope passes it over or depends on a param it
180
+ # does not meet.
181
+ def validate_iterated!(params)
182
+ # we collect errors inside array because
183
+ # there may be more than one error per field
184
+ array_errors = nil
185
+
186
+ @iterator.each(params) do |val, element|
187
+ next if passed_over?(val, element)
188
+ next unless scope.meets_dependency?(val, params)
189
+
190
+ array_errors = validate_element_attributes(val, array_errors)
191
+ end
192
+
193
+ raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
194
+ end
195
+
196
+ # Runs #validate_param! for each attribute +element+ holds, or for every
197
+ # one when they are required, and returns +array_errors+ with what
198
+ # failed appended, allocating it on the first failure.
199
+ def validate_element_attributes(element, array_errors)
200
+ @attrs.each do |attr_name|
201
+ validate_param!(attr_name, element) if required? || (hash_like?(element) && element.key?(attr_name))
202
+ rescue Grape::Exceptions::Validation => e
203
+ (array_errors ||= []) << e
204
+ end
205
+ array_errors
206
+ end
207
+
208
+ # Whether #validate! leaves +val+ alone: in an optional scope, an empty
209
+ # value that is not an element the request sent (see AttributesIterator).
210
+ def passed_over?(val, element)
211
+ !scope.required? && !element && empty_element?(val)
212
+ end
213
+
214
+ # Whether #validate_elements! leaves +elements+ alone: an optional
215
+ # scope's Array whose elements are all empty, as the iterator does.
216
+ def passed_over_elements?(elements)
217
+ !scope.required? && elements.all? { |element| empty_element?(element) }
218
+ end
219
+
220
+ # What an optional scope passes over: its params given empty in an
221
+ # element of an Array further up, or the placeholder +map_params+ puts
222
+ # where they were not given at all. The iterator drops that placeholder
223
+ # itself; #validate_elements! meets it when a scope above was handed an
224
+ # Array instead of a Hash. An empty element of the scope's own Array is
225
+ # passed over only when every element of that Array is empty.
226
+ def empty_element?(element)
227
+ return true if ParamsScope::EmptyOptionalValue.equal?(element)
228
+
229
+ element.respond_to?(:empty?) ? element.empty? : element.nil?
130
230
  end
131
231
 
132
232
  def validation_error!(attr_name_or_params, message = exception_message)
@@ -161,10 +261,9 @@ module Grape
161
261
  options_key?(:value) ? options[:value] : options
162
262
  end
163
263
 
264
+ # Listed among the helpers a custom validator can call (see README).
164
265
  def scrub(value)
165
- return value if !value.respond_to?(:valid_encoding?) || value.valid_encoding?
166
-
167
- value.scrub
266
+ Grape::Util::Scrub.scrub(value)
168
267
  end
169
268
  end
170
269
  end
@@ -11,8 +11,7 @@ module Grape
11
11
 
12
12
  @exception_message = options.message if options.message
13
13
 
14
- raw_type = options.type
15
- type = hash_like?(raw_type) ? raw_type[:value] : raw_type
14
+ type = options.type
16
15
  @converter =
17
16
  if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
18
17
  type
@@ -35,7 +34,7 @@ module Grape
35
34
  validation_error!(attr_name, new_value.message || exception_message) if new_value.is_a?(Types::InvalidValue)
36
35
 
37
36
  # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
38
- # which looses wrappers for hashes and arrays after reassigning values
37
+ # which loses wrappers for hashes and arrays after reassigning values
39
38
  #
40
39
  # h = Hashie::Mash.new(list: [1, 2, 3, 4])
41
40
  # => #<Hashie::Mash list=#<Hashie::Array [1, 2, 3, 4]>>
@@ -18,10 +18,26 @@ module Grape
18
18
  end
19
19
 
20
20
  def validate!(params)
21
- @iterator.each(params) do |resource_params, attr_name|
21
+ scoped = scope.params(params) if @direct
22
+ return apply_defaults(scoped) if scoped.is_a?(Hash)
23
+
24
+ @iterator.each(params) do |resource_params|
22
25
  next unless scope.meets_dependency?(resource_params, params)
23
26
 
24
- resource_params[attr_name] = @default_call.call(resource_params) if hash_like?(resource_params) && resource_params[attr_name].nil?
27
+ apply_defaults(resource_params) if hash_like?(resource_params)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Also #validate!'s whole job where Base#validate_attributes! applies:
34
+ # a scope that is always validated and does not iterate elements, once
35
+ # its params resolved to a Hash -- the root scope of every +params+
36
+ # block with a default in it. A scope like that depends on no other
37
+ # param, so the iterator and its dependency check have nothing to add.
38
+ def apply_defaults(params)
39
+ @attrs.each do |attr_name|
40
+ params[attr_name] = @default_call.call(params) if params[attr_name].nil?
25
41
  end
26
42
  end
27
43
  end
@@ -13,14 +13,19 @@ module Grape
13
13
  raise ArgumentError, 'except_values Proc must have arity of zero (use values: with a one-arity predicate for per-element checks)' if except_proc && !except.arity.zero?
14
14
 
15
15
  # Zero-arity procs (e.g. -> { User.pluck(:role) }) must be called per-request,
16
- # not at definition time, so they are wrapped in a lambda to defer execution.
17
- @excepts_call = except_proc ? except : -> { except }
16
+ # not at definition time. Anything else is the collection itself, held
17
+ # as it came rather than behind a lambda that would only hand it back.
18
+ if except_proc
19
+ @excepts_call = except
20
+ else
21
+ @excepts = except
22
+ end
18
23
  end
19
24
 
20
25
  def validate_param!(attr_name, params)
21
26
  return unless hash_like?(params) && params.key?(attr_name)
22
27
 
23
- excepts = @excepts_call.call
28
+ excepts = current_excepts
24
29
  return if excepts.nil?
25
30
 
26
31
  param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
@@ -28,6 +33,14 @@ module Grape
28
33
 
29
34
  validation_error!(attr_name)
30
35
  end
36
+
37
+ private
38
+
39
+ # The values ruled out, asked of the Proc on every request when they
40
+ # come from one.
41
+ def current_excepts
42
+ @excepts_call ? @excepts_call.call : @excepts
43
+ end
31
44
  end
32
45
  end
33
46
  end