grape 3.3.4 → 4.0.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/CONTRIBUTING.md +16 -0
  4. data/README.md +208 -8
  5. data/UPGRADING.md +756 -6
  6. data/lib/grape/api/instance.rb +40 -33
  7. data/lib/grape/content_types.rb +47 -1
  8. data/lib/grape/dry_types.rb +1 -1
  9. data/lib/grape/dsl/callbacks.rb +3 -9
  10. data/lib/grape/dsl/declared.rb +4 -4
  11. data/lib/grape/dsl/desc.rb +18 -8
  12. data/lib/grape/dsl/entity.rb +37 -19
  13. data/lib/grape/dsl/helpers.rb +3 -3
  14. data/lib/grape/dsl/inside_route.rb +20 -8
  15. data/lib/grape/dsl/middleware.rb +3 -3
  16. data/lib/grape/dsl/parameters.rb +54 -28
  17. data/lib/grape/dsl/request_response.rb +37 -39
  18. data/lib/grape/dsl/rescue_options.rb +6 -5
  19. data/lib/grape/dsl/routing.rb +116 -49
  20. data/lib/grape/dsl/settings.rb +1 -1
  21. data/lib/grape/dsl/validations.rb +3 -3
  22. data/lib/grape/dsl/version_options.rb +3 -3
  23. data/lib/grape/endpoint/options.rb +13 -7
  24. data/lib/grape/endpoint.rb +141 -118
  25. data/lib/grape/env.rb +7 -0
  26. data/lib/grape/error_formatter/base.rb +6 -5
  27. data/lib/grape/error_formatter.rb +6 -2
  28. data/lib/grape/exceptions/error_response.rb +4 -1
  29. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  30. data/lib/grape/exceptions/validation.rb +9 -8
  31. data/lib/grape/formatter/json.rb +1 -0
  32. data/lib/grape/formatter/serializable_hash.rb +1 -0
  33. data/lib/grape/locale/en.yml +1 -0
  34. data/lib/grape/middleware/auth/dsl.rb +5 -33
  35. data/lib/grape/middleware/error.rb +153 -46
  36. data/lib/grape/middleware/formatter.rb +95 -26
  37. data/lib/grape/middleware/precomputed_content_types.rb +12 -7
  38. data/lib/grape/middleware/stack.rb +13 -4
  39. data/lib/grape/middleware/versioner/base.rb +3 -9
  40. data/lib/grape/middleware/versioner/path.rb +49 -7
  41. data/lib/grape/namespace.rb +3 -2
  42. data/lib/grape/path.rb +8 -69
  43. data/lib/grape/precompiled_json.rb +50 -0
  44. data/lib/grape/request.rb +36 -4
  45. data/lib/grape/router/base_route.rb +47 -10
  46. data/lib/grape/router/greedy_route.rb +5 -1
  47. data/lib/grape/router/pattern/path.rb +78 -0
  48. data/lib/grape/router/pattern.rb +60 -14
  49. data/lib/grape/router/route.rb +62 -10
  50. data/lib/grape/router.rb +100 -57
  51. data/lib/grape/serve_stream/file_body.rb +6 -0
  52. data/lib/grape/serve_stream/stream_response.rb +6 -0
  53. data/lib/grape/util/api_description.rb +10 -1
  54. data/lib/grape/util/cache.rb +21 -2
  55. data/lib/grape/util/deep_freeze.rb +1 -2
  56. data/lib/grape/util/freeze_on_new.rb +20 -0
  57. data/lib/grape/util/inheritable_setting.rb +751 -37
  58. data/lib/grape/util/media_type.rb +10 -3
  59. data/lib/grape/util/path_normalizer.rb +12 -9
  60. data/lib/grape/util/registry.rb +12 -2
  61. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  62. data/lib/grape/util/stackable_values.rb +32 -6
  63. data/lib/grape/validations/attributes_iterator.rb +21 -4
  64. data/lib/grape/validations/contract_scope.rb +8 -7
  65. data/lib/grape/validations/oneof_collector.rb +6 -10
  66. data/lib/grape/validations/params_documentation.rb +2 -2
  67. data/lib/grape/validations/params_scope.rb +66 -26
  68. data/lib/grape/validations/single_attribute_iterator.rb +5 -3
  69. data/lib/grape/validations/types/array_coercer.rb +4 -6
  70. data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
  71. data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
  72. data/lib/grape/validations/types/json.rb +1 -3
  73. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  74. data/lib/grape/validations/types/primitive_coercer.rb +12 -9
  75. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
  76. data/lib/grape/validations/types.rb +5 -5
  77. data/lib/grape/validations/validations_spec.rb +14 -4
  78. data/lib/grape/validations/validators/base.rb +4 -7
  79. data/lib/grape/validations/validators/coerce_validator.rb +8 -2
  80. data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
  81. data/lib/grape/validations/validators/length_validator.rb +4 -2
  82. data/lib/grape/validations/validators/oneof_validator.rb +2 -0
  83. data/lib/grape/validations/validators/same_as_validator.rb +1 -0
  84. data/lib/grape/version.rb +2 -1
  85. data/lib/grape.rb +23 -2
  86. metadata +10 -9
  87. data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
  88. data/lib/grape/util/base_inheritable.rb +0 -43
  89. data/lib/grape/util/inheritable_values.rb +0 -33
  90. data/lib/grape/util/reverse_stackable_values.rb +0 -15
@@ -21,11 +21,13 @@ module Grape
21
21
  Boolean = Grape::API::Boolean
22
22
 
23
23
  class << self
24
- extend Forwardable
25
-
26
24
  attr_accessor :configuration
27
25
 
28
- def_delegators :@base, :to_s
26
+ def to_s
27
+ @base&.to_s || super
28
+ end
29
+
30
+ attr_reader :base
29
31
 
30
32
  def base=(grape_api)
31
33
  @base = grape_api
@@ -52,20 +54,19 @@ module Grape
52
54
  # the headers, and the body. See [the rack specification]
53
55
  # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
54
56
  def call(env)
55
- compile!
56
- @instance.call(env)
57
+ compile!.call(env)
57
58
  end
58
59
 
60
+ # Returns the compiled instance, so callers serve the one they
61
+ # compiled rather than re-reading @instance — +change!+ can nil it
62
+ # between the two reads (see #call / #recognize_path).
59
63
  def compile!
60
- return if @instance
61
-
62
- LOCK.synchronize { @instance ||= new }
64
+ @instance || LOCK.synchronize { @instance ||= new }
63
65
  end
64
66
 
65
67
  # see Grape::Router#recognize_path
66
68
  def recognize_path(path)
67
- compile!
68
- @instance.router.recognize_path(path)
69
+ compile!.router.recognize_path(path)
69
70
  end
70
71
 
71
72
  # Wipe the compiled API so we can recompile after changes were made.
@@ -81,7 +82,7 @@ module Grape
81
82
  # Propagate any inherited params down to our endpoints, and reset any
82
83
  # compiled routes.
83
84
  endpoints.each do |e|
84
- e.inherit_settings(top_level_setting.namespace_stackable)
85
+ e.inherit_settings(top_level_setting)
85
86
  e.reset_routes!
86
87
  end
87
88
 
@@ -110,13 +111,19 @@ module Grape
110
111
 
111
112
  @router.compile!
112
113
  @router.freeze
114
+ @cascade = resolve_cascade
113
115
  end
114
116
 
115
117
  # Handle a request. See Rack documentation for what `env` is.
116
118
  def call(env)
117
119
  status, headers, response = @router.call(env)
118
- unless cascade?
119
- headers = Grape::Util::Header.new.merge(headers)
120
+ unless @cascade
121
+ # +merge!+, not +merge+: the latter is a `dup` plus a `merge!`, so the
122
+ # Header built on this line would be allocated only to be discarded.
123
+ # The copy stays because +headers+ can come from a mounted Rack app,
124
+ # which is free to hand back a frozen or shared Hash that the delete
125
+ # below must not reach into.
126
+ headers = Grape::Util::Header.new.merge!(headers)
120
127
  headers.delete('X-Cascade')
121
128
  end
122
129
 
@@ -131,12 +138,13 @@ module Grape
131
138
  # In some applications (e.g. mounting grape on rails), one might need to trap
132
139
  # errors from reaching upstream. This is effectivelly done by unsetting
133
140
  # X-Cascade. Default :cascade is true.
141
+ #
142
+ # Resolved in the constructor rather than per request: answering it walks
143
+ # the whole scope chain twice, and it reads the same settings the routes
144
+ # were compiled and frozen from -- changing those has to go through
145
+ # +change!+, which discards this instance.
134
146
  def cascade?
135
- namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
136
- return namespace_inheritable[:cascade] if namespace_inheritable.key?(:cascade)
137
- return namespace_inheritable[:version_options].cascade if namespace_inheritable[:version_options]
138
-
139
- true
147
+ @cascade
140
148
  end
141
149
 
142
150
  reset!
@@ -152,18 +160,11 @@ module Grape
152
160
  # contain already versioning information when using path versioning.
153
161
  all_routes = self.class.endpoints.flat_map(&:routes)
154
162
 
155
- # Read the settings the helper routes need from a *copy* with the
156
- # root-prefix/versioning keys stripped, so adding these routes won't
157
- # prepend versioning information again. This used to delete those keys
158
- # from the shared class-level settings and restore them in an ensure;
159
- # a request served concurrently on another instance during a runtime
160
- # recompile could observe the missing keys (e.g. via #cascade?). A
161
- # local copy keeps that mutation off the shared object.
162
- namespace_inheritable = self.class.inheritable_setting.namespace_inheritable.to_hash.except(*ROOT_PREFIX_VERSIONING_KEYS)
163
- collect_route_config_per_pattern(all_routes, namespace_inheritable)
163
+ collect_route_config_per_pattern(all_routes)
164
164
  end
165
165
 
166
- def collect_route_config_per_pattern(all_routes, namespace_inheritable)
166
+ def collect_route_config_per_pattern(all_routes)
167
+ setting = self.class.inheritable_setting
167
168
  routes_by_regexp = all_routes.group_by(&:pattern_regexp)
168
169
 
169
170
  # Build the configuration based on the first endpoint and the collection of methods supported.
@@ -172,18 +173,24 @@ module Grape
172
173
 
173
174
  last_route = routes.last # Most of the configuration is taken from the last endpoint
174
175
  allowed_methods = routes.map(&:request_method)
175
- allowed_methods |= [Rack::HEAD] if !namespace_inheritable[:do_not_route_head] && allowed_methods.include?(Rack::GET)
176
+ allowed_methods |= [Rack::HEAD] if !setting.do_not_route_head? && allowed_methods.include?(Rack::GET)
176
177
 
177
- allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods
178
- last_route.app.options_route_enabled = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS)
178
+ allow_header = setting.do_not_route_options? ? allowed_methods : [Rack::OPTIONS] | allowed_methods
179
+ last_route.app.options_route_enabled = true unless setting.do_not_route_options? || allowed_methods.include?(Rack::OPTIONS)
179
180
 
180
181
  greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:)
181
182
  @router.associate_routes(greedy_route)
182
183
  end
183
184
  end
184
185
 
185
- ROOT_PREFIX_VERSIONING_KEYS = %i[version version_options root_prefix].freeze
186
- private_constant :ROOT_PREFIX_VERSIONING_KEYS
186
+ # Backs {#cascade?}; called once, from the constructor.
187
+ def resolve_cascade
188
+ setting = self.class.inheritable_setting
189
+ return setting.cascade if setting.cascade_defined?
190
+ return setting.version_options.cascade if setting.version_options
191
+
192
+ true
193
+ end
187
194
  end
188
195
  end
189
196
  end
@@ -22,7 +22,20 @@ module Grape
22
22
  def mime_types_for(from_settings)
23
23
  return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS
24
24
 
25
- from_settings.invert.transform_keys! { |k| media_type(k) }
25
+ MimeTypesCache[from_settings]
26
+ end
27
+
28
+ # Every format under both spellings in one plain Hash, so a lookup is a
29
+ # single +Hash#[]+. +HashWithIndifferentAccess+ converted the key on every
30
+ # read instead, and this is read two or three times per request — to
31
+ # negotiate the format, and again to set the response content type.
32
+ #
33
+ # Keys arrive as Symbols: the +content_type+ DSL symbolizes what it is
34
+ # given and the defaults are Symbols. The key is stored as it came too,
35
+ # so a middleware constructed directly with String keys still answers to
36
+ # either spelling, as the indifferent hash did.
37
+ def lookup_for(from_settings)
38
+ LookupCache[from_settings]
26
39
  end
27
40
 
28
41
  # The media type of a content-type header: the part before any `;`
@@ -36,5 +49,38 @@ module Grape
36
49
  base = content_type.include?(';') ? content_type.split(';', 2).first : content_type
37
50
  base.strip
38
51
  end
52
+
53
+ # Both tables below are derived from nothing but the content-type registry,
54
+ # and one content-type-aware middleware is built per API instance — so an
55
+ # app mounting N APIs held N copies of tables it only ever reads. Keying
56
+ # the cache on the registry itself collapses them: Hash keys compare by
57
+ # value, so every API that registers the same content types shares one
58
+ # table.
59
+ #
60
+ # Both the key and the table are frozen: the caller's registry stays
61
+ # reachable (through +middleware.options[:content_types]+, among others)
62
+ # and mutating a live key would corrupt a cache that is now shared
63
+ # process-wide.
64
+
65
+ class MimeTypesCache < Grape::Util::Cache
66
+ def initialize
67
+ super
68
+ @cache = Hash.new do |h, from_settings|
69
+ h[from_settings.dup.freeze] = from_settings.invert.transform_keys! { |mime_type| Grape::ContentTypes.media_type(mime_type) }.freeze
70
+ end
71
+ end
72
+ end
73
+
74
+ class LookupCache < Grape::Util::Cache
75
+ def initialize
76
+ super
77
+ @cache = Hash.new do |h, from_settings|
78
+ h[from_settings.dup.freeze] = from_settings.each_with_object({}) do |(format, media_type), lookup|
79
+ lookup[format] = media_type
80
+ lookup[format.is_a?(String) ? format.to_sym : format.to_s] = media_type
81
+ end.freeze
82
+ end
83
+ end
84
+ end
39
85
  end
40
86
  end
@@ -48,7 +48,7 @@ module Grape
48
48
  def self.wrapped_dry_types_const_get(dry_type, type)
49
49
  dry_type.const_get(type.name, false)
50
50
  rescue NameError
51
- raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
51
+ raise ArgumentError, "type `#{type}` should support coercion via `[]`" unless type.respond_to?(:[])
52
52
  end
53
53
  end
54
54
  end
@@ -9,15 +9,9 @@ module Grape
9
9
  # after: execute the given block after the endpoint code has run except in unsuccessful
10
10
  # finally: execute the given block after the endpoint code even if unsuccessful
11
11
 
12
- {
13
- before: :befores,
14
- before_validation: :before_validations,
15
- after_validation: :after_validations,
16
- after: :afters,
17
- finally: :finallies
18
- }.each do |method_name, plural_key|
19
- define_method method_name do |&block|
20
- inheritable_setting.namespace_stackable[plural_key] = block
12
+ %i[before before_validation after_validation after finally].each do |callback_name|
13
+ define_method callback_name do |&block|
14
+ inheritable_setting.add_callback(callback_name, block)
21
15
  end
22
16
  end
23
17
  end
@@ -22,11 +22,11 @@ module Grape
22
22
  def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false)
23
23
  raise MethodNotYetAvailable unless before_filter_passed
24
24
 
25
- contract_key_map = inheritable_setting.namespace_stackable[:contract_key_map]
25
+ contract_key_map = inheritable_setting.contract_key_maps
26
26
  handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map:)
27
- declared_params = include_parent_namespaces ? inheritable_setting.route[:declared_params] : (inheritable_setting.namespace_stackable[:declared_params].last || [])
28
- renamed_params = inheritable_setting.route[:renamed_params] || {}
29
- route_params = options.dig(:route_options, :params) || {} # options = endpoint's option
27
+ declared_params = include_parent_namespaces ? inheritable_setting.route_declared_params : (inheritable_setting.declared_params.last || [])
28
+ renamed_params = inheritable_setting.route_renamed_params
29
+ route_params = config.params
30
30
 
31
31
  handler.call(passed_params, declared_params, route_params, renamed_params)
32
32
  end
@@ -19,6 +19,9 @@ module Grape
19
19
  # successful call to this action
20
20
  # @option options :http_codes [Array[Array]] possible HTTP codes this
21
21
  # endpoint may return, with their meanings, in a 2d array
22
+ # @option options :default_response [Hash] the definition and entity used
23
+ # to present this endpoint's default response. Was `:default`, which is
24
+ # deprecated.
22
25
  # @option options :named [String] a specific name to help find this route
23
26
  # @option options :body_name [String] override the autogenerated body name param
24
27
  # @option options :headers [Hash] HTTP headers this method can accept
@@ -50,21 +53,28 @@ module Grape
50
53
  # # ...
51
54
  # end
52
55
  #
53
- def desc(description, *legacy_options, **options, &config_block)
54
- if legacy_options.any?
55
- Grape.deprecator.warn('Passing a positional options Hash to `desc` is deprecated. Pass keyword arguments instead.')
56
- options = legacy_options.first.merge(options)
57
- end
56
+ def desc(description, **options, &config_block)
57
+ resolved_options =
58
+ if options.key?(:default)
59
+ Grape.deprecator.warn('The `default` option of `desc` is deprecated. Use `default_response` instead.')
60
+ # Rebuilt rather than mutated in place: an explicit +default_response+
61
+ # still wins, since the merged Hash is the one that keeps its key.
62
+ { default_response: options[:default] }.merge(options.except(:default))
63
+ else
64
+ options
65
+ end
58
66
 
59
67
  settings =
60
68
  if config_block
61
69
  endpoint_config = defined?(configuration) ? configuration : nil
62
70
  Grape::Util::ApiDescription.new(description, endpoint_config, &config_block).settings
63
71
  else
64
- options.merge(description:)
72
+ resolved_options.merge(description:)
65
73
  end
66
- inheritable_setting.namespace[:description] = settings
67
- inheritable_setting.route[:description] = settings
74
+ # Only the route scope is consumed downstream (by +route+ and the
75
+ # route's readers, e.g. +http_codes+); the namespace scope was
76
+ # write-only, so it is no longer populated.
77
+ inheritable_setting.route_description = settings
68
78
  end
69
79
  end
70
80
  end
@@ -34,7 +34,7 @@ module Grape
34
34
  if key
35
35
  representation = body&.merge(key => representation) || { key => representation }
36
36
  elsif entity_class.present? && body
37
- raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge)
37
+ raise ArgumentError, "representation of type `#{representation.class}` cannot be merged" unless representation.respond_to?(:merge)
38
38
 
39
39
  representation = body.merge(representation)
40
40
  end
@@ -45,34 +45,52 @@ module Grape
45
45
  # Attempt to locate the Entity class for a given object, if not given
46
46
  # explicitly. This is done by looking for the presence of Klass::Entity,
47
47
  # where Klass is the class of the `object` parameter, or one of its
48
- # ancestors.
48
+ # ancestors. Object is excluded from the search: top-level constants
49
+ # live on it, so a global ::Entity class is not a representer.
49
50
  # @param object [Object] the object to locate the Entity class for
50
51
  # @return [Class] the located Entity class, or nil if none is found
51
52
  def entity_class_for_obj(object)
52
- klass = object_class(object)
53
-
54
- representations = inheritable_setting.namespace_stackable_with_hash(:representations)
55
- if representations
56
- potential = klass.ancestors.detect { |potential| representations.key?(potential) }
57
- return representations[potential] if potential && representations[potential]
58
- end
59
-
60
- return unless klass.const_defined?(:Entity)
61
-
62
- entity = klass.const_get(:Entity)
63
- entity if entity.respond_to?(:represent)
53
+ entity_for_class(object.class) || entity_for_class(element_class(object))
64
54
  end
65
55
 
66
56
  private
67
57
 
68
- # Resolves the class used to look up the Entity for +object+.
58
+ # The class standing in for a collection or wrapper: ActiveRecord::Relation
59
+ # and the like expose #klass, anything else falls back to the class of its
60
+ # first element.
61
+ #
62
+ # Consulted only once the object's own class has come up empty, because
63
+ # both tests are duck-typed and plenty of single objects answer them —
64
+ # a Struct is Enumerable, so it responds to #first, and so does any model
65
+ # that includes Enumerable. Asking this first meant `represent Model,
66
+ # with: Entity` was silently ignored for those, the entity for the
67
+ # *element* type being looked up instead. Deferring it also keeps #first
68
+ # from being called at all when the object resolves on its own class.
69
+ #
69
70
  # @param object [Object] the object to represent.
70
- # @return [Class] the object's collection element class, wrapped class, or its own class.
71
- def object_class(object)
71
+ # @return [Class, nil]
72
+ def element_class(object)
72
73
  return object.klass if object.respond_to?(:klass)
73
- return object.first.class if object.respond_to?(:first)
74
74
 
75
- object.class
75
+ object.first.class if object.respond_to?(:first)
76
+ end
77
+
78
+ # @param klass [Class, nil] the class to look an entity up for.
79
+ # @return [Class, nil] the registered or conventionally named entity.
80
+ def entity_for_class(klass)
81
+ return if klass.nil?
82
+
83
+ representations = inheritable_setting.representations
84
+ if representations
85
+ potential = klass.ancestors.detect { |ancestor| representations.key?(ancestor) }
86
+ return representations[potential] if potential && representations[potential]
87
+ end
88
+
89
+ owner = klass.ancestors.detect { |ancestor| ancestor != Object && ancestor.const_defined?(:Entity, false) }
90
+ return unless owner
91
+
92
+ entity = owner.const_get(:Entity, false)
93
+ entity if entity.respond_to?(:represent)
76
94
  end
77
95
 
78
96
  # @param entity_class [Class] the entity class to use for representation.
@@ -52,12 +52,12 @@ module Grape
52
52
  def make_inclusion(mod, &)
53
53
  define_boolean_in_mod(mod)
54
54
  inject_api_helpers_to_mod(mod, &)
55
- inheritable_setting.namespace_stackable[:helpers] = mod
55
+ inheritable_setting.add_helper(mod)
56
56
  end
57
57
 
58
58
  def include_all_in_scope
59
59
  mod = Module.new
60
- namespace_stackable(:helpers).each { |mod_to_include| mod.include mod_to_include }
60
+ inheritable_setting.helpers.each { |mod_to_include| mod.include mod_to_include }
61
61
  change!
62
62
  mod
63
63
  end
@@ -115,7 +115,7 @@ module Grape
115
115
  def process_named_params
116
116
  return if @named_params.blank?
117
117
 
118
- api.inheritable_setting.namespace_stackable[:named_params] = @named_params
118
+ api.inheritable_setting.add_named_params(@named_params)
119
119
  end
120
120
  end
121
121
  end
@@ -15,7 +15,7 @@ module Grape
15
15
  end
16
16
 
17
17
  def configuration
18
- config.for.configuration.evaluate
18
+ config.api.configuration.evaluate
19
19
  end
20
20
 
21
21
  # End the request and display an error to the
@@ -27,10 +27,10 @@ module Grape
27
27
  # @param backtrace [Array<String>] The backtrace of the exception that caused the error.
28
28
  # @param original_exception [Exception] The original exception that caused the error.
29
29
  def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil)
30
- status = self.status(status || inheritable_setting.namespace_inheritable[:default_error_status])
30
+ resolved_status = self.status(status || inheritable_setting.default_error_status)
31
31
  headers = additional_headers.present? ? header.merge(additional_headers) : header
32
32
  throw :error, Grape::Exceptions::ErrorResponse.new(
33
- message:, status:, headers:, backtrace:, original_exception:
33
+ message:, status: resolved_status, headers:, backtrace:, original_exception:
34
34
  )
35
35
  end
36
36
 
@@ -53,6 +53,12 @@ module Grape
53
53
  end
54
54
  header 'Location', url
55
55
  content_type 'text/plain'
56
+ # Render the message Grape generated as the plain text it is. Setting
57
+ # only the header left it to the API's own formatter, which on a JSON
58
+ # API returned the sentence wrapped in quotes under a text/plain content
59
+ # type. A caller-supplied body keeps the API's format: it may be
60
+ # structured, and the txt formatter would render a Hash through `to_s`.
61
+ api_format :txt unless body
56
62
  body body_message
57
63
  end
58
64
 
@@ -66,7 +72,7 @@ module Grape
66
72
  when Symbol, Integer
67
73
  @status = Rack::Utils.status_code(status)
68
74
  else
69
- raise ArgumentError, 'Status code must be Integer or Symbol.'
75
+ raise ArgumentError, 'status code must be Integer or Symbol'
70
76
  end
71
77
  end
72
78
 
@@ -122,7 +128,7 @@ module Grape
122
128
  def sendfile(value = nil)
123
129
  return stream if value.nil?
124
130
 
125
- raise ArgumentError, 'Argument must be a file path' unless value.is_a?(String)
131
+ raise ArgumentError, 'argument must be a file path' unless value.is_a?(String)
126
132
 
127
133
  file_body = Grape::ServeStream::FileBody.new(value)
128
134
  @stream = Grape::ServeStream::StreamResponse.new(file_body)
@@ -185,15 +191,21 @@ module Grape
185
191
  def stream_body(value)
186
192
  return Grape::ServeStream::FileBody.new(value) if value.is_a?(String)
187
193
 
188
- raise ArgumentError, 'Stream object must respond to :each.' unless value.respond_to?(:each)
194
+ raise ArgumentError, 'stream object must respond to :each' unless value.respond_to?(:each)
189
195
 
190
196
  value
191
197
  end
192
198
 
193
199
  # The default HTTP status when none has been set explicitly.
200
+ # Reads the request method once instead of asking through +post?+ and
201
+ # +delete?+, each of which reads it again. Every response that did not set
202
+ # a status of its own comes through here, so it is read straight off the
203
+ # env: +Grape::Request+ wraps the very same Hash and answers
204
+ # +request_method+ with the same lookup, two method calls further down.
194
205
  def default_status
195
- return 201 if request.post?
196
- return 204 if request.delete? && @body.blank?
206
+ request_method = env[Rack::REQUEST_METHOD]
207
+ return 201 if request_method == Rack::POST
208
+ return 204 if request_method == Rack::DELETE && @body.blank?
197
209
 
198
210
  200
199
211
  end
@@ -13,7 +13,7 @@ module Grape
13
13
  arr = [:use, middleware_class, *args]
14
14
  arr << block if block
15
15
 
16
- inheritable_setting.namespace_stackable[:middleware] = arr
16
+ inheritable_setting.add_middleware(arr)
17
17
  end
18
18
 
19
19
  %i[insert insert_before insert_after].each do |method_name|
@@ -21,7 +21,7 @@ module Grape
21
21
  arr = [method_name, *args]
22
22
  arr << block if block
23
23
 
24
- inheritable_setting.namespace_stackable[:middleware] = arr
24
+ inheritable_setting.add_middleware(arr)
25
25
  end
26
26
  end
27
27
 
@@ -29,7 +29,7 @@ module Grape
29
29
  # and arguments that are currently applied to the
30
30
  # application.
31
31
  def middleware
32
- inheritable_setting.namespace_stackable[:middleware] || []
32
+ inheritable_setting.middleware
33
33
  end
34
34
  end
35
35
  end
@@ -29,7 +29,7 @@ module Grape
29
29
  # end
30
30
  # end
31
31
  def build_with(build_with)
32
- @api.inheritable_setting.namespace_inheritable[:build_params_with] = build_with
32
+ @api.inheritable_setting.build_params_with = build_with
33
33
  end
34
34
 
35
35
  # Include reusable params rules among current.
@@ -54,7 +54,9 @@ module Grape
54
54
  # end
55
55
  # end
56
56
  def use(*names, **options)
57
- named_params = @api.inheritable_setting.namespace_stackable_with_hash(:named_params) || {}
57
+ return redispatch_legacy_options(:use, names, options) if legacy_options?(names)
58
+
59
+ named_params = @api.inheritable_setting.named_params || {}
58
60
  names.each do |name|
59
61
  params_block = named_params.fetch(name) do
60
62
  raise "Params :#{name} not found!"
@@ -74,8 +76,8 @@ module Grape
74
76
  #
75
77
  # @param attrs list of parameters names, or, if :using is
76
78
  # passed as an option, which keys to include (:all or :none) from
77
- # the :using hash. The last key can be a hash, which specifies
78
- # options for the parameters
79
+ # the :using hash. Passing the options as a trailing positional Hash
80
+ # is deprecated; pass them as keyword arguments instead.
79
81
  # @option attrs :type [Class] the type to coerce this parameter to before
80
82
  # passing it to the endpoint. See {Grape::Validations::Types} for a list of
81
83
  # types that are supported automatically. Custom classes may be used
@@ -122,34 +124,20 @@ module Grape
122
124
  # requires :name, type: String
123
125
  # end
124
126
  # end
125
- def requires(*attrs, using: nil, except: nil, **opts, &block)
126
- opts[:presence] = { value: true, message: opts[:message] }
127
- opts = @group.deep_merge(opts) if @group
128
-
129
- return require_required_and_optional_fields(attrs.first, using:, except:) if using
127
+ def requires(*attrs, using: nil, except: nil, as: nil, **opts, &)
128
+ return redispatch_legacy_options(:requires, attrs, { using:, except:, as: }.compact.merge(opts), &) if legacy_options?(attrs)
130
129
 
131
- validate_attributes(attrs, **opts, &block)
132
- block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], &block) : push_declared_params(attrs, as: opts[:as])
130
+ declare(attrs, opts, required: true, using:, except:, as:, &)
133
131
  end
134
132
 
135
133
  # Allow, but don't require, one or more parameters for the current
136
134
  # endpoint.
137
135
  # @param (see #requires)
138
136
  # @option (see #requires)
139
- def optional(*attrs, using: nil, except: nil, **opts, &block)
140
- type = opts[:type]
141
- opts = @group.deep_merge(opts) if @group
142
-
143
- # check type for optional parameter group
144
- if attrs && block
145
- raise Grape::Exceptions::MissingGroupType if type.nil?
146
- raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
147
- end
148
-
149
- return require_optional_fields(attrs.first, using:, except:) if using
137
+ def optional(*attrs, using: nil, except: nil, as: nil, **opts, &)
138
+ return redispatch_legacy_options(:optional, attrs, { using:, except:, as: }.compact.merge(opts), &) if legacy_options?(attrs)
150
139
 
151
- validate_attributes(attrs, **opts, &block)
152
- block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], optional: true, &block) : push_declared_params(attrs, as: opts[:as])
140
+ declare(attrs, opts, required: false, using:, except:, as:, &)
153
141
  end
154
142
 
155
143
  # Define common settings for one or more parameters
@@ -162,7 +150,7 @@ module Grape
162
150
 
163
151
  %i[mutually_exclusive exactly_one_of at_least_one_of all_or_none_of].each do |validator|
164
152
  define_method validator do |*attrs, message: nil|
165
- validates(attrs, validator => { value: true, message: })
153
+ validates(attrs, { validator => { value: true, message: } })
166
154
  end
167
155
  end
168
156
 
@@ -203,13 +191,51 @@ module Grape
203
191
  # @return hash of parameters relevant for the current scope
204
192
  # @api private
205
193
  def params(params)
206
- params = @parent.qualifying_params.presence || @parent.params(params) if @parent
207
- params = map_params(params, @element) if @element
208
- params
194
+ scoped = @parent ? (@parent.qualifying_params.presence || @parent.params(params)) : params
195
+ @element ? map_params(scoped, @element) : scoped
209
196
  end
210
197
 
211
198
  private
212
199
 
200
+ # @deprecated A trailing positional options Hash is deprecated; pass keyword
201
+ # arguments instead. Before Ruby 3 keyword separation this Hash was pulled
202
+ # off the argument list by `extract_options!`; now it lands in the splat and
203
+ # would silently be treated as a parameter name.
204
+ # +requires+ and +optional+ differ only in whether what they declare is
205
+ # required, so the declaration itself lives here.
206
+ #
207
+ # The two +using:+ helpers stay separate. They are not one computation
208
+ # with a flag flipped: +requires+ treats +except+ as "these go in the
209
+ # other bucket", while +optional+ has no other bucket — +:all+ ignores
210
+ # +except+ and +:none+ uses it to drop fields entirely.
211
+ def declare(attrs, opts, required:, using:, except:, as:, &block)
212
+ merged_opts = @group&.deep_merge(opts) || opts
213
+ declared_as = as || merged_opts[:as]
214
+
215
+ if using
216
+ return require_required_and_optional_fields(attrs.first, using:, except:) if required
217
+
218
+ return require_optional_fields(attrs.first, using:, except:)
219
+ end
220
+
221
+ validates(attrs, merged_opts, required:)
222
+ return push_declared_params(attrs, as: declared_as) unless block
223
+
224
+ new_scope(attrs.first, type: merged_opts[:type], as: declared_as, optional: !required, &block)
225
+ end
226
+
227
+ def legacy_options?(args)
228
+ args.size > 1 && args.last.is_a?(Hash)
229
+ end
230
+
231
+ # Re-invokes +method_name+ with the trailing Hash splatted as keyword
232
+ # arguments, so Ruby routes its keys to the same place they would have
233
+ # reached had the caller omitted the braces.
234
+ def redispatch_legacy_options(method_name, args, opts, &)
235
+ Grape.deprecator.warn("Passing a positional options Hash to `#{method_name}` is deprecated. Pass keyword arguments instead.")
236
+ __send__(method_name, *args[0..-2], **args.last.merge(opts), &)
237
+ end
238
+
213
239
  def first_hash_key_or_param(parameter)
214
240
  parameter.is_a?(Hash) ? parameter.keys.first : parameter
215
241
  end