grape 2.4.0 → 4.0.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +351 -0
  3. data/CONTRIBUTING.md +18 -10
  4. data/README.md +457 -228
  5. data/UPGRADING.md +1240 -1
  6. data/grape.gemspec +7 -7
  7. data/lib/grape/api/instance.rb +65 -116
  8. data/lib/grape/api.rb +53 -62
  9. data/lib/grape/content_types.rb +58 -3
  10. data/lib/grape/cookies.rb +2 -6
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -4
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +24 -67
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/helpers.rb +80 -65
  18. data/lib/grape/dsl/inside_route.rb +56 -296
  19. data/lib/grape/dsl/logger.rb +4 -9
  20. data/lib/grape/dsl/middleware.rb +22 -40
  21. data/lib/grape/dsl/parameters.rb +86 -93
  22. data/lib/grape/dsl/request_response.rb +139 -139
  23. data/lib/grape/dsl/rescue_options.rb +25 -0
  24. data/lib/grape/dsl/routing.rb +328 -203
  25. data/lib/grape/dsl/settings.rb +29 -135
  26. data/lib/grape/dsl/validations.rb +38 -44
  27. data/lib/grape/dsl/version_options.rb +23 -0
  28. data/lib/grape/endpoint/options.rb +25 -0
  29. data/lib/grape/endpoint.rb +285 -245
  30. data/lib/grape/env.rb +8 -3
  31. data/lib/grape/error_formatter/base.rb +27 -21
  32. data/lib/grape/error_formatter/json.rb +6 -0
  33. data/lib/grape/error_formatter/txt.rb +10 -10
  34. data/lib/grape/error_formatter.rb +6 -2
  35. data/lib/grape/exceptions/base.rb +20 -44
  36. data/lib/grape/exceptions/error_response.rb +48 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  38. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  39. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  40. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  41. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  42. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  44. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  45. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  46. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  47. data/lib/grape/exceptions/request_error.rb +11 -0
  48. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  49. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  50. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  51. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  52. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  53. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  54. data/lib/grape/exceptions/validation.rb +25 -7
  55. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  56. data/lib/grape/exceptions/validation_errors.rb +21 -23
  57. data/lib/grape/formatter/json.rb +1 -0
  58. data/lib/grape/formatter/serializable_hash.rb +6 -9
  59. data/lib/grape/json.rb +38 -2
  60. data/lib/grape/locale/en.yml +3 -5
  61. data/lib/grape/middleware/auth/base.rb +4 -3
  62. data/lib/grape/middleware/auth/dsl.rb +9 -23
  63. data/lib/grape/middleware/base.rb +41 -36
  64. data/lib/grape/middleware/error.rb +271 -73
  65. data/lib/grape/middleware/formatter.rb +152 -63
  66. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  67. data/lib/grape/middleware/stack.rb +20 -11
  68. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  69. data/lib/grape/middleware/versioner/base.rb +40 -48
  70. data/lib/grape/middleware/versioner/header.rb +5 -23
  71. data/lib/grape/middleware/versioner/param.rb +2 -2
  72. data/lib/grape/middleware/versioner/path.rb +54 -7
  73. data/lib/grape/mountable.rb +22 -0
  74. data/lib/grape/namespace.rb +19 -11
  75. data/lib/grape/params_builder/base.rb +2 -0
  76. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  77. data/lib/grape/params_builder.rb +2 -19
  78. data/lib/grape/parser/json.rb +1 -1
  79. data/lib/grape/path.rb +8 -72
  80. data/lib/grape/precompiled_json.rb +50 -0
  81. data/lib/grape/request.rb +57 -20
  82. data/lib/grape/router/base_route.rb +53 -7
  83. data/lib/grape/router/greedy_route.rb +16 -6
  84. data/lib/grape/router/mustermann_pattern.rb +44 -0
  85. data/lib/grape/router/pattern/path.rb +78 -0
  86. data/lib/grape/router/pattern.rb +65 -37
  87. data/lib/grape/router/route.rb +66 -18
  88. data/lib/grape/router.rb +126 -115
  89. data/lib/grape/serve_stream/file_body.rb +7 -0
  90. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  91. data/lib/grape/serve_stream/stream_response.rb +7 -0
  92. data/lib/grape/testing.rb +33 -0
  93. data/lib/grape/util/api_description.rb +67 -0
  94. data/lib/grape/util/cache.rb +22 -2
  95. data/lib/grape/util/deep_freeze.rb +34 -0
  96. data/lib/grape/util/freeze_on_new.rb +20 -0
  97. data/lib/grape/util/inheritable_setting.rb +777 -39
  98. data/lib/grape/util/lazy/base.rb +16 -0
  99. data/lib/grape/util/lazy/block.rb +2 -9
  100. data/lib/grape/util/lazy/value.rb +2 -9
  101. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  102. data/lib/grape/util/media_type.rb +13 -9
  103. data/lib/grape/util/path_normalizer.rb +37 -0
  104. data/lib/grape/util/registry.rb +13 -3
  105. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  106. data/lib/grape/util/stackable_values.rb +39 -10
  107. data/lib/grape/util/translation.rb +42 -0
  108. data/lib/grape/validations/attributes_iterator.rb +61 -29
  109. data/lib/grape/validations/coerce_options.rb +21 -0
  110. data/lib/grape/validations/contract_scope.rb +8 -13
  111. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  112. data/lib/grape/validations/oneof_collector.rb +35 -0
  113. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  114. data/lib/grape/validations/params_documentation.rb +52 -0
  115. data/lib/grape/validations/params_scope.rb +217 -304
  116. data/lib/grape/validations/shared_options.rb +19 -0
  117. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  118. data/lib/grape/validations/types/array_coercer.rb +7 -10
  119. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  120. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  121. data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
  122. data/lib/grape/validations/types/json.rb +1 -3
  123. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  124. data/lib/grape/validations/types/primitive_coercer.rb +14 -33
  125. data/lib/grape/validations/types/set_coercer.rb +1 -1
  126. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  127. data/lib/grape/validations/types.rb +29 -51
  128. data/lib/grape/validations/validations_spec.rb +164 -0
  129. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  130. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  131. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  132. data/lib/grape/validations/validators/base.rb +118 -34
  133. data/lib/grape/validations/validators/coerce_validator.rb +25 -37
  134. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
  135. data/lib/grape/validations/validators/default_validator.rb +13 -20
  136. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  137. data/lib/grape/validations/validators/except_values_validator.rb +14 -4
  138. data/lib/grape/validations/validators/length_validator.rb +23 -22
  139. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  140. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  141. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  142. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  143. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  144. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  145. data/lib/grape/validations/validators/values_validator.rb +34 -26
  146. data/lib/grape/version.rb +1 -2
  147. data/lib/grape/xml.rb +8 -1
  148. data/lib/grape.rb +58 -31
  149. metadata +62 -39
  150. data/lib/grape/api/helpers.rb +0 -9
  151. data/lib/grape/dsl/api.rb +0 -17
  152. data/lib/grape/dsl/configuration.rb +0 -15
  153. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  154. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  155. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  156. data/lib/grape/exceptions/missing_option.rb +0 -11
  157. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  158. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  159. data/lib/grape/exceptions/unknown_options.rb +0 -11
  160. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  161. data/lib/grape/extensions/hash.rb +0 -27
  162. data/lib/grape/extensions/hashie/mash.rb +0 -24
  163. data/lib/grape/middleware/globals.rb +0 -14
  164. data/lib/grape/types/invalid_value.rb +0 -8
  165. data/lib/grape/util/base_inheritable.rb +0 -43
  166. data/lib/grape/util/inheritable_values.rb +0 -29
  167. data/lib/grape/util/reverse_stackable_values.rb +0 -15
  168. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  169. data/lib/grape/validations/attributes_doc.rb +0 -60
  170. data/lib/grape/validations/validator_factory.rb +0 -15
data/grape.gemspec CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- $LOAD_PATH.unshift File.expand_path('lib', __dir__)
4
- require 'grape/version'
3
+ require_relative 'lib/grape/version'
5
4
 
6
5
  Gem::Specification.new do |s|
7
6
  s.name = 'grape'
@@ -21,13 +20,14 @@ Gem::Specification.new do |s|
21
20
  'rubygems_mfa_required' => 'true'
22
21
  }
23
22
 
24
- s.add_dependency 'activesupport', '>= 6.1'
23
+ s.add_dependency 'activesupport', '>= 7.2'
24
+ s.add_dependency 'dry-configurable', '>= 1.0'
25
25
  s.add_dependency 'dry-types', '>= 1.1'
26
- s.add_dependency 'mustermann-grape', '~> 1.1.0'
27
- s.add_dependency 'rack', '>= 2'
28
- s.add_dependency 'zeitwerk'
26
+ s.add_dependency 'mustermann', '>= 4.0'
27
+ s.add_dependency 'rack', '>= 2.2.4'
28
+ s.add_dependency 'zeitwerk', '>= 2.6'
29
29
 
30
30
  s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec']
31
31
  s.require_paths = ['lib']
32
- s.required_ruby_version = '>= 2.7.0'
32
+ s.required_ruby_version = '>= 3.3'
33
33
  end
@@ -5,32 +5,37 @@ module Grape
5
5
  # The API Instance class, is the engine behind Grape::API. Each class that inherits
6
6
  # from this will represent a different API instance
7
7
  class Instance
8
+ # Marks this and every mounted instance as a mountable Grape app (see Grape::Mountable).
9
+ extend Grape::Mountable
10
+ extend Grape::DSL::Settings
11
+ extend Grape::DSL::Desc
12
+ extend Grape::DSL::Validations
13
+ extend Grape::DSL::Callbacks
14
+ extend Grape::DSL::Logger
15
+ extend Grape::DSL::Middleware
16
+ extend Grape::DSL::RequestResponse
17
+ extend Grape::DSL::Routing
18
+ extend Grape::DSL::Helpers
8
19
  extend Grape::Middleware::Auth::DSL
9
- include Grape::DSL::API
20
+
21
+ Boolean = Grape::API::Boolean
10
22
 
11
23
  class << self
12
- attr_reader :instance, :base
13
24
  attr_accessor :configuration
14
25
 
15
- def given(conditional_option, &block)
16
- evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block
26
+ def to_s
27
+ @base&.to_s || super
17
28
  end
18
29
 
19
- def mounted(&block)
20
- evaluate_as_instance_with_configuration(block, lazy: true)
21
- end
30
+ attr_reader :base
22
31
 
23
32
  def base=(grape_api)
24
33
  @base = grape_api
25
34
  grape_api.instances << self
26
35
  end
27
36
 
28
- def to_s
29
- base&.to_s || super
30
- end
31
-
32
37
  def base_instance?
33
- self == base.base_instance
38
+ self == @base.base_instance
34
39
  end
35
40
 
36
41
  # A class-level lock to ensure the API is not compiled by multiple
@@ -44,92 +49,32 @@ module Grape
44
49
  reset_validations!
45
50
  end
46
51
 
47
- # Parses the API's definition and compiles it into an instance of
48
- # Grape::API.
49
- def compile
50
- @instance ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
51
- end
52
-
53
- # Wipe the compiled API so we can recompile after changes were made.
54
- def change!
55
- @instance = nil
56
- end
57
-
58
52
  # This is the interface point between Rack and Grape; it accepts a request
59
53
  # from Rack and ultimately returns an array of three values: the status,
60
54
  # the headers, and the body. See [the rack specification]
61
55
  # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
62
56
  def call(env)
63
- compile!
64
- call!(env)
65
- end
66
-
67
- # A non-synchronized version of ::call.
68
- def call!(env)
69
- instance.call(env)
70
- end
71
-
72
- # (see #cascade?)
73
- def cascade(value = nil)
74
- if value.nil?
75
- inheritable_setting.namespace_inheritable.key?(:cascade) ? !namespace_inheritable(:cascade).nil? : true
76
- else
77
- namespace_inheritable(:cascade, value)
78
- end
57
+ compile!.call(env)
79
58
  end
80
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).
81
63
  def compile!
82
- return if instance
83
-
84
- LOCK.synchronize { compile unless instance }
64
+ @instance || LOCK.synchronize { @instance ||= new }
85
65
  end
86
66
 
87
67
  # see Grape::Router#recognize_path
88
68
  def recognize_path(path)
89
- compile!
90
- instance.router.recognize_path(path)
91
- end
92
-
93
- protected
94
-
95
- def prepare_routes
96
- endpoints.map(&:routes).flatten
97
- end
98
-
99
- # Execute first the provided block, then each of the
100
- # block passed in. Allows for simple 'before' setups
101
- # of settings stack pushes.
102
- def nest(*blocks, &block)
103
- blocks.compact!
104
- if blocks.any?
105
- evaluate_as_instance_with_configuration(block) if block
106
- blocks.each { |b| evaluate_as_instance_with_configuration(b) }
107
- reset_validations!
108
- else
109
- instance_eval(&block)
110
- end
69
+ compile!.router.recognize_path(path)
111
70
  end
112
71
 
113
- def evaluate_as_instance_with_configuration(block, lazy: false)
114
- lazy_block = Grape::Util::Lazy::Block.new do |configuration|
115
- value_for_configuration = configuration
116
- self.configuration = value_for_configuration.evaluate if value_for_configuration.try(:lazy?)
117
- response = instance_eval(&block)
118
- self.configuration = value_for_configuration
119
- response
120
- end
121
- if base && base_instance? && lazy
122
- lazy_block
123
- else
124
- lazy_block.evaluate_from(configuration)
125
- end
72
+ # Wipe the compiled API so we can recompile after changes were made.
73
+ def change!
74
+ @instance = nil
126
75
  end
127
76
 
128
- def inherited(subclass)
129
- super
130
- subclass.reset!
131
- subclass.logger = logger.clone
132
- end
77
+ protected
133
78
 
134
79
  def inherit_settings(other_settings)
135
80
  top_level_setting.inherit_from other_settings.point_in_time_copy
@@ -137,12 +82,20 @@ module Grape
137
82
  # Propagate any inherited params down to our endpoints, and reset any
138
83
  # compiled routes.
139
84
  endpoints.each do |e|
140
- e.inherit_settings(top_level_setting.namespace_stackable)
85
+ e.inherit_settings(top_level_setting)
141
86
  e.reset_routes!
142
87
  end
143
88
 
144
89
  reset_routes!
145
90
  end
91
+
92
+ private
93
+
94
+ def inherited(subclass)
95
+ super
96
+ subclass.reset!
97
+ subclass.logger logger.clone
98
+ end
146
99
  end
147
100
 
148
101
  attr_reader :router
@@ -158,13 +111,19 @@ module Grape
158
111
 
159
112
  @router.compile!
160
113
  @router.freeze
114
+ @cascade = resolve_cascade
161
115
  end
162
116
 
163
117
  # Handle a request. See Rack documentation for what `env` is.
164
118
  def call(env)
165
119
  status, headers, response = @router.call(env)
166
- unless cascade?
167
- 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)
168
127
  headers.delete('X-Cascade')
169
128
  end
170
129
 
@@ -179,11 +138,13 @@ module Grape
179
138
  # In some applications (e.g. mounting grape on rails), one might need to trap
180
139
  # errors from reaching upstream. This is effectivelly done by unsetting
181
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.
182
146
  def cascade?
183
- return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.key?(:cascade)
184
- return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options)&.key?(:cascade)
185
-
186
- true
147
+ @cascade
187
148
  end
188
149
 
189
150
  reset!
@@ -197,50 +158,38 @@ module Grape
197
158
  def add_head_not_allowed_methods_and_options_methods
198
159
  # The paths we collected are prepared (cf. Path#prepare), so they
199
160
  # contain already versioning information when using path versioning.
200
- all_routes = self.class.endpoints.map(&:routes).flatten
161
+ all_routes = self.class.endpoints.flat_map(&:routes)
201
162
 
202
- # Disable versioning so adding a route won't prepend versioning
203
- # informations again.
204
- without_root_prefix_and_versioning { collect_route_config_per_pattern(all_routes) }
163
+ collect_route_config_per_pattern(all_routes)
205
164
  end
206
165
 
207
166
  def collect_route_config_per_pattern(all_routes)
167
+ setting = self.class.inheritable_setting
208
168
  routes_by_regexp = all_routes.group_by(&:pattern_regexp)
209
169
 
210
170
  # Build the configuration based on the first endpoint and the collection of methods supported.
211
171
  routes_by_regexp.each_value do |routes|
212
- last_route = routes.last # Most of the configuration is taken from the last endpoint
213
172
  next if routes.any? { |route| route.request_method == '*' }
214
173
 
174
+ last_route = routes.last # Most of the configuration is taken from the last endpoint
215
175
  allowed_methods = routes.map(&:request_method)
216
- allowed_methods |= [Rack::HEAD] if !self.class.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)
217
177
 
218
- allow_header = self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Rack::OPTIONS] | allowed_methods
219
- last_route.app.options[:options_route_enabled] = true unless self.class.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)
220
180
 
221
- @router.associate_routes(last_route.pattern, {
222
- endpoint: last_route.app,
223
- allow_header: allow_header
224
- })
181
+ greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:)
182
+ @router.associate_routes(greedy_route)
225
183
  end
226
184
  end
227
185
 
228
- # Allows definition of endpoints that ignore the versioning configuration
229
- # used by the rest of your API.
230
- def without_root_prefix_and_versioning
231
- old_version = self.class.namespace_inheritable(:version)
232
- old_version_options = self.class.namespace_inheritable(:version_options)
233
- old_root_prefix = self.class.namespace_inheritable(:root_prefix)
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
234
191
 
235
- self.class.namespace_inheritable_to_nil(:version)
236
- self.class.namespace_inheritable_to_nil(:version_options)
237
- self.class.namespace_inheritable_to_nil(:root_prefix)
238
-
239
- yield
240
-
241
- self.class.namespace_inheritable(:version, old_version)
242
- self.class.namespace_inheritable(:version_options, old_version_options)
243
- self.class.namespace_inheritable(:root_prefix, old_root_prefix)
192
+ true
244
193
  end
245
194
  end
246
195
  end
data/lib/grape/api.rb CHANGED
@@ -4,43 +4,42 @@ module Grape
4
4
  # The API class is the primary entry point for creating Grape APIs. Users
5
5
  # should subclass this class in order to build an API.
6
6
  class API
7
+ # Marks this and every subclass as a mountable Grape app (see Grape::Mountable).
8
+ extend Grape::Mountable
9
+
7
10
  # Class methods that we want to call on the API rather than on the API object
8
- NON_OVERRIDABLE = %i[call call! configuration compile! inherited recognize_path].freeze
11
+ NON_OVERRIDABLE = %i[base base= base_instance? call change! configuration compile! inherit_settings recognize_path reset! routes top_level_setting= top_level_setting].freeze
12
+
13
+ Helpers = Grape::DSL::Helpers::BaseHelper
9
14
 
10
15
  class Boolean
16
+ VALUES = [true, false].freeze
17
+ private_constant :VALUES
18
+
11
19
  def self.build(val)
12
- return nil if val != true && val != false
20
+ return unless VALUES.include?(val)
13
21
 
14
22
  new
15
23
  end
16
24
  end
17
25
 
18
- class Instance
19
- Boolean = Grape::API::Boolean
20
- end
21
-
22
26
  class << self
23
27
  extend Forwardable
28
+
24
29
  attr_accessor :base_instance, :instances
25
30
 
26
31
  delegate_missing_to :base_instance
27
- def_delegators :base_instance, :new, :configuration
28
32
 
29
33
  # This is the interface point between Rack and Grape; it accepts a request
30
34
  # from Rack and ultimately returns an array of three values: the status,
31
35
  # the headers, and the body. See [the rack specification]
32
- # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
36
+ # (https://github.com/rack/rack/blob/main/SPEC.rdoc) for more.
33
37
  # NOTE: This will only be called on an API directly mounted on RACK
34
- def_delegators :instance_for_rack, :call, :compile!
35
-
36
- # When inherited, will create a list of all instances (times the API was mounted)
37
- # It will listen to the setup required to mount that endpoint, and replicate it on any new instance
38
- def inherited(api)
39
- super
40
-
41
- api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
42
- api.override_all_methods!
43
- end
38
+ #
39
+ # +inherit_settings+ is left out: it is protected on the base instance, so
40
+ # a delegator would raise NoMethodError just as the missing one does.
41
+ def_delegators :base_instance, :new, :configuration, :call, :change!, :compile!, :recognize_path, :routes,
42
+ :base, :base=, :base_instance?, :reset!, :top_level_setting
44
43
 
45
44
  # Initialize the instance variables on the remountable class, and the base_instance
46
45
  # an instance that will be used to create the set up but will not be mounted
@@ -54,8 +53,8 @@ module Grape
54
53
  # Redefines all methods so that are forwarded to add_setup and be recorded
55
54
  def override_all_methods!
56
55
  (base_instance.methods - Class.methods - NON_OVERRIDABLE).each do |method_override|
57
- define_singleton_method(method_override) do |*args, &block|
58
- add_setup(method: method_override, args: args, block: block)
56
+ define_singleton_method(method_override) do |*args, **kwargs, &block|
57
+ add_setup(method: method_override, args:, kwargs:, block:)
59
58
  end
60
59
  end
61
60
  end
@@ -67,12 +66,10 @@ module Grape
67
66
  # `configuration` as normal.
68
67
  def configure
69
68
  config = @base_instance.configuration
70
- if block_given?
71
- yield config
72
- self
73
- else
74
- config
75
- end
69
+ return config unless block_given?
70
+
71
+ yield config
72
+ self
76
73
  end
77
74
 
78
75
  # The remountable class can have a configuration hash to provide some dynamic class-level variables.
@@ -80,15 +77,24 @@ module Grape
80
77
  # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
81
78
  # too much, you may actually want to provide a new API rather than remount it.
82
79
  def mount_instance(configuration: nil)
83
- Class.new(@base_parent).tap do |instance|
84
- instance.configuration = Grape::Util::EndpointConfiguration.new(configuration || {})
85
- instance.base = self
86
- replay_setup_on(instance)
87
- end
80
+ instance = Class.new(@base_parent)
81
+ instance.configuration = Grape::Util::EndpointConfiguration.new(configuration || {})
82
+ instance.base = self
83
+ replay_setup_on(instance)
84
+ instance
88
85
  end
89
86
 
90
87
  private
91
88
 
89
+ # When inherited, will create a list of all instances (times the API was mounted)
90
+ # It will listen to the setup required to mount that endpoint, and replicate it on any new instance
91
+ def inherited(api)
92
+ super
93
+
94
+ api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
95
+ api.override_all_methods!
96
+ end
97
+
92
98
  # Replays the set up to produce an API as defined in this class, can be called
93
99
  # on classes that inherit from Grape::API
94
100
  def replay_setup_on(instance)
@@ -97,16 +103,8 @@ module Grape
97
103
  end
98
104
  end
99
105
 
100
- def instance_for_rack
101
- if never_mounted?
102
- base_instance
103
- else
104
- mounted_instances.first
105
- end
106
- end
107
-
108
106
  # Adds a new stage to the set up require to get a Grape::API up and running
109
- def add_setup(step)
107
+ def add_setup(**step)
110
108
  @setup << step
111
109
  last_response = nil
112
110
  @instances.each do |instance|
@@ -130,49 +128,42 @@ module Grape
130
128
  end
131
129
  end
132
130
 
133
- def replay_step_on(instance, method:, args:, block:)
134
- return if skip_immediate_run?(instance, args)
131
+ def replay_step_on(instance, method:, args:, kwargs:, block:)
132
+ return if skip_immediate_run?(instance, args, kwargs)
135
133
 
136
134
  eval_args = evaluate_arguments(instance.configuration, *args)
137
- response = instance.send(method, *eval_args, &block)
138
- if skip_immediate_run?(instance, [response])
139
- response
140
- else
141
- evaluate_arguments(instance.configuration, response).first
142
- end
135
+ eval_kwargs = kwargs.deep_transform_values { |v| evaluate_arguments(instance.configuration, v).first }
136
+ response = instance.__send__(method, *eval_args, **eval_kwargs, &block)
137
+
138
+ return response if skip_immediate_run?(instance, [response], kwargs)
139
+
140
+ evaluate_arguments(instance.configuration, response).first
143
141
  end
144
142
 
145
143
  # Skips steps that contain arguments to be lazily executed (on re-mount time)
146
- def skip_immediate_run?(instance, args)
144
+ def skip_immediate_run?(instance, args, kwargs)
147
145
  instance.base_instance? &&
148
- (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) })
146
+ (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) } || any_lazy?(kwargs.values))
149
147
  end
150
148
 
151
149
  def any_lazy?(args)
152
- args.any? { |argument| argument.try(:lazy?) }
150
+ args.any?(Grape::Util::Lazy::Base)
153
151
  end
154
152
 
155
153
  def evaluate_arguments(configuration, *args)
156
154
  args.map do |argument|
157
- if argument.try(:lazy?)
155
+ case argument
156
+ when Grape::Util::Lazy::Base
158
157
  argument.evaluate_from(configuration)
159
- elsif argument.is_a?(Hash)
158
+ when Hash
160
159
  argument.transform_values { |value| evaluate_arguments(configuration, value).first }
161
- elsif argument.is_a?(Array)
160
+ when Array
162
161
  evaluate_arguments(configuration, *argument)
163
162
  else
164
163
  argument
165
164
  end
166
165
  end
167
166
  end
168
-
169
- def never_mounted?
170
- mounted_instances.empty?
171
- end
172
-
173
- def mounted_instances
174
- instances - [base_instance]
175
- end
176
167
  end
177
168
  end
178
169
  end
@@ -22,9 +22,64 @@ 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.each_with_object({}) do |(k, v), types_without_params|
26
- # remove optional parameter
27
- types_without_params[v.split(';', 2).first] = 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]
39
+ end
40
+
41
+ # The media type of a content-type header: the part before any `;`
42
+ # parameters, with surrounding whitespace removed
43
+ # (e.g. `'text/html'` for `'text/html; charset=utf-8'`). Returns nil for a
44
+ # nil content type. Skips the split (and its allocation) when there are no
45
+ # parameters, which is the common case.
46
+ def media_type(content_type)
47
+ return if content_type.nil?
48
+
49
+ base = content_type.include?(';') ? content_type.split(';', 2).first : content_type
50
+ base.strip
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
28
83
  end
29
84
  end
30
85
  end
data/lib/grape/cookies.rb CHANGED
@@ -13,7 +13,7 @@ module Grape
13
13
  def_delegators :cookies, :[], :each
14
14
 
15
15
  def initialize(rack_cookies)
16
- @cookies = rack_cookies
16
+ @cookies = ActiveSupport::HashWithIndifferentAccess.new(rack_cookies)
17
17
  @send_cookies = nil
18
18
  end
19
19
 
@@ -37,11 +37,7 @@ module Grape
37
37
 
38
38
  private
39
39
 
40
- def cookies
41
- return @cookies unless @cookies.is_a?(Proc)
42
-
43
- @cookies = @cookies.call.with_indifferent_access
44
- end
40
+ attr_reader :cookies
45
41
 
46
42
  def send_cookies
47
43
  @send_cookies ||= Set.new