grape 3.0.1 → 3.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/CONTRIBUTING.md +2 -2
  4. data/README.md +2 -2
  5. data/UPGRADING.md +14 -0
  6. data/grape.gemspec +2 -2
  7. data/lib/grape/api/instance.rb +16 -47
  8. data/lib/grape/api.rb +6 -10
  9. data/lib/grape/content_types.rb +1 -4
  10. data/lib/grape/declared_params_handler.rb +118 -0
  11. data/lib/grape/dsl/declared.rb +35 -0
  12. data/lib/grape/dsl/helpers.rb +2 -2
  13. data/lib/grape/dsl/inside_route.rb +3 -141
  14. data/lib/grape/dsl/parameters.rb +8 -26
  15. data/lib/grape/dsl/routing.rb +41 -29
  16. data/lib/grape/dsl/settings.rb +1 -1
  17. data/lib/grape/dsl/validations.rb +22 -20
  18. data/lib/grape/endpoint.rb +84 -83
  19. data/lib/grape/exceptions/base.rb +1 -1
  20. data/lib/grape/middleware/auth/dsl.rb +4 -4
  21. data/lib/grape/middleware/base.rb +4 -0
  22. data/lib/grape/middleware/error.rb +1 -1
  23. data/lib/grape/middleware/formatter.rb +6 -4
  24. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  25. data/lib/grape/middleware/versioner/base.rb +20 -0
  26. data/lib/grape/middleware/versioner/header.rb +1 -17
  27. data/lib/grape/middleware/versioner/path.rb +1 -1
  28. data/lib/grape/namespace.rb +5 -9
  29. data/lib/grape/params_builder.rb +2 -19
  30. data/lib/grape/router/base_route.rb +14 -5
  31. data/lib/grape/router/greedy_route.rb +11 -5
  32. data/lib/grape/router/pattern.rb +6 -20
  33. data/lib/grape/router/route.rb +7 -11
  34. data/lib/grape/router.rb +35 -61
  35. data/lib/grape/util/api_description.rb +10 -8
  36. data/lib/grape/validations/attributes_iterator.rb +2 -2
  37. data/lib/grape/validations/params_scope.rb +12 -10
  38. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  39. data/lib/grape/validations/validators/base.rb +2 -2
  40. data/lib/grape/validations/validators/except_values_validator.rb +1 -1
  41. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +1 -1
  42. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  43. data/lib/grape/validations/validators/regexp_validator.rb +12 -2
  44. data/lib/grape/validations/validators/values_validator.rb +1 -1
  45. data/lib/grape/version.rb +1 -1
  46. data/lib/grape.rb +5 -13
  47. metadata +11 -14
  48. data/lib/grape/exceptions/missing_option.rb +0 -11
  49. data/lib/grape/exceptions/unknown_options.rb +0 -11
  50. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  51. data/lib/grape/extensions/hash.rb +0 -27
  52. data/lib/grape/extensions/hashie/mash.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f57d6d494fcd9459d437ca723a988bfe68c7059accaec1ea2e01ddba66b0958
4
- data.tar.gz: 84aebfa13e7591da6a520b3f3287bc479371be55afa3ee7bbc06ed1c12b5089c
3
+ metadata.gz: a5d4ecfd73b009cc3c4bcfd0ed50048d70d15a20c48e5282571ef5d9dc717316
4
+ data.tar.gz: 60de796e7e411513df39ac636750d9ce22ac5f43bec24dce48627d672d63a9d8
5
5
  SHA512:
6
- metadata.gz: 58cd9a65cadb03008d053fa4f1e5259079942cf39f714ddf54d1d9f7cd2b9e1d81a2c2cddb346270e24b6a424c3adff9664e1627eb0d03f5bf6bf67d5b0b3b6f
7
- data.tar.gz: d73cb08a47287b24044aefa037ebf5e85850a4bc1e41859247416537cf5ce720936f69da42974dd2fa661a042292ed9a51b2e46d1091a221f08dc198ecb4fb27
6
+ metadata.gz: 8cedc8302caa250302cde57047e45fb20d6dcd7f03cc71aa0f5cc1544e548a9d13745264f11c6c1dbd17dc82c8cf80165a4a803877a1c8fce4e96912bfdb3c06
7
+ data.tar.gz: 25f407f60587fc6ebed6e10fc91ea74f5bf3e23846b52a20f188a755fa0caa9f224b7d03b07542855f7894b99e078441bc0556d9ed41bc89ffde0c042dc8cd50
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ### 3.1.0 (2026-01-25)
2
+
3
+ #### Features
4
+
5
+ * [#2629](https://github.com/ruby-grape/grape/pull/2629): Refactor Router Architecture - [@ericproulx](https://github.com/ericproulx).
6
+ * [#2633](https://github.com/ruby-grape/grape/pull/2633): Refactor API::Instance and reorganize DSL modules - [@ericproulx](https://github.com/ericproulx).
7
+ * [#2636](https://github.com/ruby-grape/grape/pull/2636): Refactor router to simplify method signatures and reduce duplication - [@ericproulx](https://github.com/ericproulx).
8
+ * [#2640](https://github.com/ruby-grape/grape/pull/2640): Compute available_media_types once - [@ericproulx](https://github.com/ericproulx).
9
+ * [#2637](https://github.com/ruby-grape/grape/pull/2637): Refactor declared method - [@ericproulx](https://github.com/ericproulx).
10
+ * [#2639](https://github.com/ruby-grape/grape/pull/2639): Refactor mime_types_for - [@ericproulx](https://github.com/ericproulx).
11
+ * [#2638](https://github.com/ruby-grape/grape/pull/2638): Remove unnecessary path string duplication - [@ericproulx](https://github.com/ericproulx).
12
+ * [#2643](https://github.com/ruby-grape/grape/pull/2638): Remove `try` method in codebase - [@ericproulx](https://github.com/ericproulx).
13
+ * [#2646](https://github.com/ruby-grape/grape/pull/2646): Call `valid_encoding?` before scrub - [@ericproulx](https://github.com/ericproulx).
14
+ * [#2644](https://github.com/ruby-grape/grape/pull/2644): Clean useless/not valuable dependencies - [@ericproulx](https://github.com/ericproulx).
15
+ * [#2649](https://github.com/ruby-grape/grape/pull/2644): Drop support Ruby 3.0 and ActiveSupport 7.0 - [@ericproulx](https://github.com/ericproulx).
16
+ * [#2648](https://github.com/ruby-grape/grape/pull/2648): Remove deprecated ParamsBuilders extensions - [@ericproulx](https://github.com/ericproulx).
17
+ * [#2645](https://github.com/ruby-grape/grape/pull/2645): Endpoints are compiled when API is compiled - [@ericproulx](https://github.com/ericproulx).
18
+ * [#2647](https://github.com/ruby-grape/grape/pull/2647): Explicit kwargs for `namespace` and `route_param` - [@ericproulx](https://github.com/ericproulx).
19
+ * [#2651](https://github.com/ruby-grape/grape/pull/2651): Migrate Danger to use danger-pr-comment workflow - [@dblock](https://github.com/dblock).
20
+
21
+ #### Fixes
22
+
23
+ * [#2633](https://github.com/ruby-grape/grape/pull/2633): Fix cascade reading - [@ericproulx](https://github.com/ericproulx).
24
+ * [#2641](https://github.com/ruby-grape/grape/pull/2641): Restore support for `return` in endpoint blocks - [@ericproulx](https://github.com/ericproulx).
25
+ * [#2642](https://github.com/ruby-grape/grape/pull/2642): Fix array allocation in base_route.rb - [@ericproulx](https://github.com/ericproulx).
26
+
1
27
  ### 3.0.1 (2025-11-24)
2
28
 
3
29
  #### Features
data/CONTRIBUTING.md CHANGED
@@ -48,8 +48,8 @@ Here are some examples:
48
48
  - running rspec on a specific file `docker-compose run --rm --build grape rspec spec/:file_path`
49
49
  - running task `docker-compose run --rm --build grape rake <task_name>`
50
50
  - running rubocop `docker-compose run --rm --build grape rubocop`
51
- - running all specs on a specific ruby version (e.g 3.0) `RUBY_VERSION=3.0 docker-compose run --rm --build grape rspec`
52
- - running specs on a specific gemfile (e.g rails_7_0.gemfile) `docker-compose run -e GEMFILE=rails_7_0 --rm --build grape rspec`
51
+ - running all specs on a specific ruby version (e.g 3.4) `RUBY_VERSION=3.4 docker-compose run --rm --build grape rspec`
52
+ - running specs on a specific gemfile (e.g rails_8_1.gemfile) `docker-compose run -e GEMFILE=rails_8_1 --rm --build grape rspec`
53
53
 
54
54
  #### Bundle Install and Test
55
55
 
data/README.md CHANGED
@@ -160,7 +160,7 @@ Grape is a REST-like API framework for Ruby. It's designed to run on Rack or com
160
160
 
161
161
  ## Stable Release
162
162
 
163
- You're reading the documentation for the stable release of Grape, 3.0.1.
163
+ You're reading the documentation for the stable release of Grape, 3.1.0.
164
164
 
165
165
  ## Project Resources
166
166
 
@@ -177,7 +177,7 @@ The maintainers of Grape are working with Tidelift to deliver commercial support
177
177
 
178
178
  ## Installation
179
179
 
180
- Ruby 3.0 or newer is required.
180
+ Ruby 3.1 or newer is required.
181
181
 
182
182
  Grape is available as a gem, to install it run:
183
183
 
data/UPGRADING.md CHANGED
@@ -1,6 +1,20 @@
1
1
  Upgrading Grape
2
2
  ===============
3
3
 
4
+ ### Upgrading to >= 3.1
5
+
6
+ #### Explicit kwargs for `namespace` and `route_param`
7
+
8
+ The `API#namespace` and `route_param` methods are now defined with `**options` instead of `options = {}`. In addtion, `requirements` in explicitly defined so it's not in `options` anymore. You can still call `requirements` like before but `options[:requirements]` will be empty. For `route_param`, `type` is also an explicit parameter so it's not in `options` anymore. See [#2647](https://github.com/ruby-grape/grape/pull/2647) for more information.
9
+
10
+ #### ParamsBuilder Grape::Extensions
11
+
12
+ Deprecated [ParamsBuilder's extensions](https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#params-builder) have been removed.
13
+
14
+ #### Enhanced API compile!
15
+
16
+ Endpoints are now "compiled" instead of lazy loaded. Historically, when calling `YourAPI.compile!` in `config.ru` (or just receiving the first API call), only routing was compiled see [Grape::Router#compile!](https://github.com/ruby-grape/grape/blob/bf90e95c3b17c415c944363b1c07eb9727089ee7/lib/grape/router.rb#L41-L54) and endpoints were lazy loaded. Now, it's part of the API compilation. See [#2645](https://github.com/ruby-grape/grape/pull/2645) for more information.
17
+
4
18
  ### Upgrading to >= 3.0.0
5
19
 
6
20
  #### Ruby 3+ Argument Delegation Modernization
data/grape.gemspec CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  'rubygems_mfa_required' => 'true'
21
21
  }
22
22
 
23
- s.add_dependency 'activesupport', '>= 7.0'
23
+ s.add_dependency 'activesupport', '>= 7.1'
24
24
  s.add_dependency 'dry-configurable'
25
25
  s.add_dependency 'dry-types', '>= 1.1'
26
26
  s.add_dependency 'mustermann-grape', '~> 1.1.0'
@@ -29,5 +29,5 @@ Gem::Specification.new do |s|
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 = '>= 3.0'
32
+ s.required_ruby_version = '>= 3.1'
33
33
  end
@@ -21,20 +21,9 @@ module Grape
21
21
  class << self
22
22
  extend Forwardable
23
23
 
24
- attr_reader :instance, :base
25
24
  attr_accessor :configuration
26
25
 
27
- def_delegators :base, :to_s
28
-
29
- def given(conditional_option, &block)
30
- return unless conditional_option
31
-
32
- mounted(&block)
33
- end
34
-
35
- def mounted(&block)
36
- evaluate_as_instance_with_configuration(block, lazy: true)
37
- end
26
+ def_delegators :@base, :to_s
38
27
 
39
28
  def base=(grape_api)
40
29
  @base = grape_api
@@ -42,7 +31,7 @@ module Grape
42
31
  end
43
32
 
44
33
  def base_instance?
45
- self == base.base_instance
34
+ self == @base.base_instance
46
35
  end
47
36
 
48
37
  # A class-level lock to ensure the API is not compiled by multiple
@@ -56,43 +45,30 @@ module Grape
56
45
  reset_validations!
57
46
  end
58
47
 
59
- # Parses the API's definition and compiles it into an instance of
60
- # Grape::API.
61
- def compile
62
- @instance ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
63
- end
64
-
65
48
  # This is the interface point between Rack and Grape; it accepts a request
66
49
  # from Rack and ultimately returns an array of three values: the status,
67
50
  # the headers, and the body. See [the rack specification]
68
51
  # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
69
52
  def call(env)
70
53
  compile!
71
- call!(env)
72
- end
73
-
74
- # A non-synchronized version of ::call.
75
- def call!(env)
76
- instance.call(env)
77
- end
78
-
79
- # (see #cascade?)
80
- def cascade(value = nil)
81
- return inheritable_setting.namespace_inheritable.key?(:cascade) ? !inheritable_setting.namespace_inheritable(:cascade).nil? : true if value.nil?
82
-
83
- inheritable_setting.namespace_inheritable[:cascade] = value
54
+ @instance.call(env)
84
55
  end
85
56
 
86
57
  def compile!
87
- return if instance
58
+ return if @instance
88
59
 
89
- LOCK.synchronize { compile unless instance }
60
+ LOCK.synchronize { @instance ||= new }
90
61
  end
91
62
 
92
63
  # see Grape::Router#recognize_path
93
64
  def recognize_path(path)
94
65
  compile!
95
- instance.router.recognize_path(path)
66
+ @instance.router.recognize_path(path)
67
+ end
68
+
69
+ # Wipe the compiled API so we can recompile after changes were made.
70
+ def change!
71
+ @instance = nil
96
72
  end
97
73
 
98
74
  protected
@@ -110,11 +86,6 @@ module Grape
110
86
  reset_routes!
111
87
  end
112
88
 
113
- # Wipe the compiled API so we can recompile after changes were made.
114
- def change!
115
- @instance = nil
116
- end
117
-
118
89
  private
119
90
 
120
91
  def inherited(subclass)
@@ -177,7 +148,7 @@ module Grape
177
148
  def add_head_not_allowed_methods_and_options_methods
178
149
  # The paths we collected are prepared (cf. Path#prepare), so they
179
150
  # contain already versioning information when using path versioning.
180
- all_routes = self.class.endpoints.map(&:routes).flatten
151
+ all_routes = self.class.endpoints.flat_map(&:routes)
181
152
 
182
153
  # Disable versioning so adding a route won't prepend versioning
183
154
  # informations again.
@@ -186,23 +157,21 @@ module Grape
186
157
 
187
158
  def collect_route_config_per_pattern(all_routes)
188
159
  routes_by_regexp = all_routes.group_by(&:pattern_regexp)
160
+ namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
189
161
 
190
162
  # Build the configuration based on the first endpoint and the collection of methods supported.
191
163
  routes_by_regexp.each_value do |routes|
192
- last_route = routes.last # Most of the configuration is taken from the last endpoint
193
164
  next if routes.any? { |route| route.request_method == '*' }
194
165
 
195
- namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
166
+ last_route = routes.last # Most of the configuration is taken from the last endpoint
196
167
  allowed_methods = routes.map(&:request_method)
197
168
  allowed_methods |= [Rack::HEAD] if !namespace_inheritable[:do_not_route_head] && allowed_methods.include?(Rack::GET)
198
169
 
199
170
  allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods
200
171
  last_route.app.options[:options_route_enabled] = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS)
201
172
 
202
- @router.associate_routes(last_route.pattern, {
203
- endpoint: last_route.app,
204
- allow_header: allow_header
205
- })
173
+ greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header: allow_header)
174
+ @router.associate_routes(greedy_route)
206
175
  end
207
176
  end
208
177
 
data/lib/grape/api.rb CHANGED
@@ -5,7 +5,7 @@ module Grape
5
5
  # should subclass this class in order to build an API.
6
6
  class API
7
7
  # 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 routes].freeze
8
+ NON_OVERRIDABLE = %i[base= base_instance? call change! configuration compile! inherit_settings recognize_path reset! routes top_level_setting= top_level_setting].freeze
9
9
 
10
10
  Helpers = Grape::DSL::Helpers::BaseHelper
11
11
 
@@ -29,7 +29,7 @@ module Grape
29
29
  # the headers, and the body. See [the rack specification]
30
30
  # (https://github.com/rack/rack/blob/main/SPEC.rdoc) for more.
31
31
  # NOTE: This will only be called on an API directly mounted on RACK
32
- def_delegators :base_instance, :new, :configuration, :call, :compile!
32
+ def_delegators :base_instance, :new, :configuration, :call, :change!, :compile!, :recognize_path, :routes
33
33
 
34
34
  # Initialize the instance variables on the remountable class, and the base_instance
35
35
  # an instance that will be used to create the set up but will not be mounted
@@ -140,12 +140,12 @@ module Grape
140
140
  end
141
141
 
142
142
  def any_lazy?(args)
143
- args.any? { |argument| argument.try(:lazy?) }
143
+ args.any? { |argument| argument_lazy?(argument) }
144
144
  end
145
145
 
146
146
  def evaluate_arguments(configuration, *args)
147
147
  args.map do |argument|
148
- if argument.try(:lazy?)
148
+ if argument_lazy?(argument)
149
149
  argument.evaluate_from(configuration)
150
150
  elsif argument.is_a?(Hash)
151
151
  argument.transform_values { |value| evaluate_arguments(configuration, value).first }
@@ -157,12 +157,8 @@ module Grape
157
157
  end
158
158
  end
159
159
 
160
- def never_mounted?
161
- mounted_instances.empty?
162
- end
163
-
164
- def mounted_instances
165
- instances - [base_instance]
160
+ def argument_lazy?(argument)
161
+ argument.respond_to?(:lazy?) && argument.lazy?
166
162
  end
167
163
  end
168
164
  end
@@ -22,10 +22,7 @@ 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
28
- end
25
+ from_settings.invert.transform_keys! { |k| k.include?(';') ? k.split(';', 2).first : k }
29
26
  end
30
27
  end
31
28
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class DeclaredParamsHandler
5
+ def initialize(include_missing: true, evaluate_given: false, stringify: false, contract_key_map: nil)
6
+ @include_missing = include_missing
7
+ @evaluate_given = evaluate_given
8
+ @stringify = stringify
9
+ @contract_key_map = contract_key_map
10
+ end
11
+
12
+ def call(passed_params, declared_params, route_params, renamed_params)
13
+ recursive_declared(
14
+ passed_params,
15
+ declared_params: declared_params,
16
+ route_params: route_params,
17
+ renamed_params: renamed_params
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def recursive_declared(passed_params, declared_params:, route_params:, renamed_params:, params_nested_path: [])
24
+ res = if passed_params.is_a?(Array)
25
+ passed_params.map do |passed_param|
26
+ recursive_declared(passed_param, declared_params:, params_nested_path:, renamed_params:, route_params:)
27
+ end
28
+ else
29
+ declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:)
30
+ end
31
+
32
+ @contract_key_map&.each { |key_map| key_map.write(passed_params, res) }
33
+
34
+ res
35
+ end
36
+
37
+ def declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:)
38
+ declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
39
+ next if @evaluate_given && !declared_param_attr.scope.attr_meets_dependency?(passed_params)
40
+
41
+ declared_hash_attr(
42
+ passed_params,
43
+ declared_param: declared_param_attr.key,
44
+ params_nested_path:,
45
+ memo:,
46
+ renamed_params:,
47
+ route_params:
48
+ )
49
+ end
50
+ end
51
+
52
+ def declared_hash_attr(passed_params, declared_param:, params_nested_path:, memo:, renamed_params:, route_params:)
53
+ if declared_param.is_a?(Hash)
54
+ declared_param.each_pair do |declared_parent_param, declared_children_params|
55
+ next unless @include_missing || passed_params.key?(declared_parent_param)
56
+
57
+ memo_key = build_memo_key(params_nested_path, declared_parent_param, renamed_params)
58
+ passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
59
+
60
+ params_nested_path_dup = params_nested_path.dup
61
+ params_nested_path_dup << declared_parent_param.to_s
62
+
63
+ memo[memo_key] = handle_passed_param(params_nested_path_dup, route_params:, has_passed_children: passed_children_params.any?) do
64
+ recursive_declared(
65
+ passed_children_params,
66
+ declared_params: declared_children_params,
67
+ params_nested_path: params_nested_path_dup,
68
+ renamed_params:,
69
+ route_params:
70
+ )
71
+ end
72
+ end
73
+ else
74
+ # If it is not a Hash then it does not have children.
75
+ # Find its value or set it to nil.
76
+ return unless @include_missing || (passed_params.respond_to?(:key?) && passed_params.key?(declared_param))
77
+
78
+ memo_key = build_memo_key(params_nested_path, declared_param, renamed_params)
79
+ passed_param = passed_params[declared_param]
80
+
81
+ params_nested_path_dup = params_nested_path.dup
82
+ params_nested_path_dup << declared_param.to_s
83
+
84
+ memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup, route_params:) do
85
+ passed_param
86
+ end
87
+ end
88
+ end
89
+
90
+ def build_memo_key(params_nested_path, declared_param, renamed_params)
91
+ rename_path = params_nested_path + [declared_param.to_s]
92
+ renamed_param_name = renamed_params[rename_path]
93
+
94
+ param = renamed_param_name || declared_param
95
+ @stringify ? param.to_s : param.to_sym
96
+ end
97
+
98
+ def handle_passed_param(params_nested_path, route_params:, has_passed_children: false, &_block)
99
+ return yield if has_passed_children
100
+
101
+ key = params_nested_path[0]
102
+ key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
103
+
104
+ type = route_params.dig(key, :type)
105
+ has_children = route_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
106
+
107
+ if type == 'Hash' && !has_children
108
+ {}
109
+ elsif type == 'Array' || (type&.start_with?('[') && !type.include?(','))
110
+ []
111
+ elsif type == 'Set' || type&.start_with?('#<Set', 'Set')
112
+ Set.new
113
+ else
114
+ yield
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ module Declared
6
+ # Denotes a situation where a DSL method has been invoked in a
7
+ # filter which it should not yet be available in
8
+ class MethodNotYetAvailable < StandardError
9
+ def initialize(msg = '#declared is not available prior to parameter validation')
10
+ super
11
+ end
12
+ end
13
+
14
+ # A filtering method that will return a hash
15
+ # consisting only of keys that have been declared by a
16
+ # `params` statement against the current/target endpoint or parent
17
+ # namespaces.
18
+ # @param params [Hash] The initial hash to filter. Usually this will just be `params`
19
+ # @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
20
+ # options. `:include_parent_namespaces` defaults to true, hence must be set to false if
21
+ # you want only to return params declared against the current/target endpoint.
22
+ def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false)
23
+ raise MethodNotYetAvailable unless before_filter_passed
24
+
25
+ contract_key_map = inheritable_setting.namespace_stackable[:contract_key_map]
26
+ handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map: 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
30
+
31
+ handler.call(passed_params, declared_params, route_params, renamed_params)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -50,9 +50,9 @@ module Grape
50
50
  end
51
51
  end
52
52
 
53
- def make_inclusion(mod, &block)
53
+ def make_inclusion(mod, &)
54
54
  define_boolean_in_mod(mod)
55
- inject_api_helpers_to_mod(mod, &block)
55
+ inject_api_helpers_to_mod(mod, &)
56
56
  inheritable_setting.namespace_stackable[:helpers] = mod
57
57
  end
58
58
 
@@ -3,148 +3,10 @@
3
3
  module Grape
4
4
  module DSL
5
5
  module InsideRoute
6
- # Denotes a situation where a DSL method has been invoked in a
7
- # filter which it should not yet be available in
8
- class MethodNotYetAvailable < StandardError; end
6
+ include Declared
9
7
 
10
- # @param type [Symbol] The type of filter for which evaluation has been
11
- # completed
12
- # @return [Module] A module containing method overrides suitable for the
13
- # position in the filter evaluation sequence denoted by +type+. This
14
- # defaults to an empty module if no overrides are defined for the given
15
- # filter +type+.
16
- def self.post_filter_methods(type)
17
- @post_filter_modules ||= { before: PostBeforeFilter }
18
- @post_filter_modules[type]
19
- end
20
-
21
- # Methods which should not be available in filters until the before filter
22
- # has completed
23
- module PostBeforeFilter
24
- def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
25
- options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
26
- declared_params ||= optioned_declared_params(options[:include_parent_namespaces])
27
-
28
- res = if passed_params.is_a?(Array)
29
- declared_array(passed_params, options, declared_params, params_nested_path)
30
- else
31
- declared_hash(passed_params, options, declared_params, params_nested_path)
32
- end
33
-
34
- if (key_maps = inheritable_setting.namespace_stackable[:contract_key_map])
35
- key_maps.each { |key_map| key_map.write(passed_params, res) }
36
- end
37
-
38
- res
39
- end
40
-
41
- private
42
-
43
- def declared_array(passed_params, options, declared_params, params_nested_path)
44
- passed_params.map do |passed_param|
45
- declared(passed_param || {}, options, declared_params, params_nested_path)
46
- end
47
- end
48
-
49
- def declared_hash(passed_params, options, declared_params, params_nested_path)
50
- declared_params.each_with_object(passed_params.class.new) do |declared_param_attr, memo|
51
- next if options[:evaluate_given] && !declared_param_attr.scope.attr_meets_dependency?(passed_params)
52
-
53
- declared_hash_attr(passed_params, options, declared_param_attr.key, params_nested_path, memo)
54
- end
55
- end
56
-
57
- def declared_hash_attr(passed_params, options, declared_param, params_nested_path, memo)
58
- renamed_params = inheritable_setting.route[:renamed_params] || {}
59
- if declared_param.is_a?(Hash)
60
- declared_param.each_pair do |declared_parent_param, declared_children_params|
61
- params_nested_path_dup = params_nested_path.dup
62
- params_nested_path_dup << declared_parent_param.to_s
63
- next unless options[:include_missing] || passed_params.key?(declared_parent_param)
64
-
65
- rename_path = params_nested_path + [declared_parent_param.to_s]
66
- renamed_param_name = renamed_params[rename_path]
67
-
68
- memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options)
69
- passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
70
-
71
- memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do
72
- declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
73
- end
74
- end
75
- else
76
- # If it is not a Hash then it does not have children.
77
- # Find its value or set it to nil.
78
- return unless options[:include_missing] || passed_params.try(:key?, declared_param)
79
-
80
- rename_path = params_nested_path + [declared_param.to_s]
81
- renamed_param_name = renamed_params[rename_path]
82
-
83
- memo_key = optioned_param_key(renamed_param_name || declared_param, options)
84
- passed_param = passed_params[declared_param]
85
-
86
- params_nested_path_dup = params_nested_path.dup
87
- params_nested_path_dup << declared_param.to_s
88
- memo[memo_key] = passed_param || handle_passed_param(params_nested_path_dup) do
89
- passed_param
90
- end
91
- end
92
- end
93
-
94
- def handle_passed_param(params_nested_path, has_passed_children = false, &_block)
95
- return yield if has_passed_children
96
-
97
- key = params_nested_path[0]
98
- key += "[#{params_nested_path[1..].join('][')}]" if params_nested_path.size > 1
99
-
100
- route_options_params = options[:route_options][:params] || {}
101
- type = route_options_params.dig(key, :type)
102
- has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
103
-
104
- if type == 'Hash' && !has_children
105
- {}
106
- elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(','))
107
- []
108
- elsif type == 'Set' || type&.start_with?('#<Set')
109
- Set.new
110
- else
111
- yield
112
- end
113
- end
114
-
115
- def optioned_param_key(declared_param, options)
116
- options[:stringify] ? declared_param.to_s : declared_param.to_sym
117
- end
118
-
119
- def optioned_declared_params(include_parent_namespaces)
120
- declared_params = if include_parent_namespaces
121
- # Declared params including parent namespaces
122
- inheritable_setting.route[:declared_params]
123
- else
124
- # Declared params at current namespace
125
- inheritable_setting.namespace_stackable[:declared_params].last || []
126
- end
127
-
128
- raise ArgumentError, 'Tried to filter for declared parameters but none exist.' unless declared_params
129
-
130
- declared_params
131
- end
132
- end
133
-
134
- # A filtering method that will return a hash
135
- # consisting only of keys that have been declared by a
136
- # `params` statement against the current/target endpoint or parent
137
- # namespaces.
138
- #
139
- # @see +PostBeforeFilter#declared+
140
- #
141
- # @param params [Hash] The initial hash to filter. Usually this will just be `params`
142
- # @param options [Hash] Can pass `:include_missing`, `:stringify` and `:include_parent_namespaces`
143
- # options. `:include_parent_namespaces` defaults to true, hence must be set to false if
144
- # you want only to return params declared against the current/target endpoint.
145
- def declared(*)
146
- raise MethodNotYetAvailable, '#declared is not available prior to parameter validation.'
147
- end
8
+ # Backward compatibility: alias exception class to previous location
9
+ MethodNotYetAvailable = Declared::MethodNotYetAvailable
148
10
 
149
11
  # The API version as specified in the URL.
150
12
  def version