grape 2.4.0 → 3.2.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +109 -0
  3. data/CONTRIBUTING.md +2 -10
  4. data/README.md +137 -181
  5. data/UPGRADING.md +154 -0
  6. data/grape.gemspec +4 -4
  7. data/lib/grape/api/instance.rb +51 -104
  8. data/lib/grape/api.rb +29 -42
  9. data/lib/grape/content_types.rb +1 -4
  10. data/lib/grape/declared_params_handler.rb +118 -0
  11. data/lib/grape/dry_types.rb +48 -4
  12. data/lib/grape/dsl/callbacks.rb +8 -58
  13. data/lib/grape/dsl/declared.rb +35 -0
  14. data/lib/grape/dsl/desc.rb +8 -67
  15. data/lib/grape/dsl/helpers.rb +59 -64
  16. data/lib/grape/dsl/inside_route.rb +28 -189
  17. data/lib/grape/dsl/logger.rb +3 -6
  18. data/lib/grape/dsl/middleware.rb +22 -40
  19. data/lib/grape/dsl/parameters.rb +24 -51
  20. data/lib/grape/dsl/request_response.rb +136 -139
  21. data/lib/grape/dsl/routing.rb +240 -200
  22. data/lib/grape/dsl/settings.rb +23 -135
  23. data/lib/grape/dsl/validations.rb +38 -44
  24. data/lib/grape/endpoint.rb +169 -205
  25. data/lib/grape/error_formatter/base.rb +4 -2
  26. data/lib/grape/exceptions/base.rb +19 -45
  27. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  28. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  29. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  30. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  31. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  32. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  33. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  34. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  35. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  36. data/lib/grape/exceptions/request_error.rb +11 -0
  37. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  38. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  39. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  40. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  41. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  42. data/lib/grape/exceptions/validation.rb +7 -4
  43. data/lib/grape/exceptions/validation_errors.rb +13 -7
  44. data/lib/grape/locale/en.yml +0 -5
  45. data/lib/grape/middleware/auth/base.rb +2 -0
  46. data/lib/grape/middleware/auth/dsl.rb +9 -10
  47. data/lib/grape/middleware/base.rb +6 -4
  48. data/lib/grape/middleware/error.rb +4 -14
  49. data/lib/grape/middleware/formatter.rb +11 -7
  50. data/lib/grape/middleware/stack.rb +3 -2
  51. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  52. data/lib/grape/middleware/versioner/base.rb +42 -40
  53. data/lib/grape/middleware/versioner/header.rb +2 -18
  54. data/lib/grape/middleware/versioner/param.rb +2 -2
  55. data/lib/grape/middleware/versioner/path.rb +2 -2
  56. data/lib/grape/namespace.rb +15 -8
  57. data/lib/grape/params_builder/base.rb +2 -0
  58. data/lib/grape/params_builder.rb +2 -19
  59. data/lib/grape/request.rb +2 -10
  60. data/lib/grape/router/base_route.rb +14 -5
  61. data/lib/grape/router/greedy_route.rb +11 -5
  62. data/lib/grape/router/pattern.rb +6 -20
  63. data/lib/grape/router/route.rb +7 -11
  64. data/lib/grape/router.rb +42 -65
  65. data/lib/grape/util/api_description.rb +58 -0
  66. data/lib/grape/util/base_inheritable.rb +5 -2
  67. data/lib/grape/util/cache.rb +1 -0
  68. data/lib/grape/util/deep_freeze.rb +35 -0
  69. data/lib/grape/util/inheritable_setting.rb +8 -1
  70. data/lib/grape/util/media_type.rb +2 -2
  71. data/lib/grape/util/registry.rb +1 -1
  72. data/lib/grape/util/translation.rb +42 -0
  73. data/lib/grape/validations/attributes_iterator.rb +35 -20
  74. data/lib/grape/validations/contract_scope.rb +2 -8
  75. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  76. data/lib/grape/validations/param_scope_tracker.rb +57 -0
  77. data/lib/grape/validations/params_documentation.rb +50 -0
  78. data/lib/grape/validations/params_scope.rb +147 -156
  79. data/lib/grape/validations/single_attribute_iterator.rb +2 -2
  80. data/lib/grape/validations/types/array_coercer.rb +2 -3
  81. data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
  82. data/lib/grape/validations/types/primitive_coercer.rb +1 -28
  83. data/lib/grape/validations/types.rb +10 -25
  84. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  85. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  86. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  87. data/lib/grape/validations/validators/base.rb +95 -25
  88. data/lib/grape/validations/validators/coerce_validator.rb +15 -35
  89. data/lib/grape/validations/validators/contract_scope_validator.rb +9 -5
  90. data/lib/grape/validations/validators/default_validator.rb +12 -18
  91. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  92. data/lib/grape/validations/validators/except_values_validator.rb +13 -4
  93. data/lib/grape/validations/validators/length_validator.rb +21 -22
  94. data/lib/grape/validations/validators/multiple_params_base.rb +5 -5
  95. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  96. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  97. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  98. data/lib/grape/validations/validators/same_as_validator.rb +6 -15
  99. data/lib/grape/validations/validators/values_validator.rb +29 -21
  100. data/lib/grape/version.rb +1 -1
  101. data/lib/grape.rb +29 -23
  102. metadata +31 -26
  103. data/lib/grape/api/helpers.rb +0 -9
  104. data/lib/grape/dsl/api.rb +0 -17
  105. data/lib/grape/dsl/configuration.rb +0 -15
  106. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  107. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  108. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  109. data/lib/grape/exceptions/missing_option.rb +0 -11
  110. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  111. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  112. data/lib/grape/exceptions/unknown_options.rb +0 -11
  113. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  114. data/lib/grape/extensions/hash.rb +0 -27
  115. data/lib/grape/extensions/hashie/mash.rb +0 -24
  116. data/lib/grape/types/invalid_value.rb +0 -8
  117. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  118. data/lib/grape/validations/attributes_doc.rb +0 -60
  119. data/lib/grape/validations/validator_factory.rb +0 -15
data/UPGRADING.md CHANGED
@@ -1,6 +1,160 @@
1
1
  Upgrading Grape
2
2
  ===============
3
3
 
4
+ ### Upgrading to >= 3.2
5
+
6
+ #### Rack parameter parsing errors now raise `Grape::Exceptions::RequestError`
7
+
8
+ Rack errors raised during parameter parsing (malformed multipart, parameter type conflicts, encoding issues, etc.) are now wrapped in `Grape::Exceptions::RequestError` instead of their previous specific exception classes (`Grape::Exceptions::EmptyMessageBody`, `Grape::Exceptions::TooManyMultipartFiles`, `Grape::Exceptions::TooDeepParameters`, `Grape::Exceptions::ConflictingTypes`, `Grape::Exceptions::InvalidParameters`). Those classes have been removed.
9
+
10
+ If you rescue any of these specific exceptions, update your rescue clauses to use `Grape::Exceptions::RequestError`:
11
+
12
+ ```ruby
13
+ # Before
14
+ rescue Grape::Exceptions::ConflictingTypes, Grape::Exceptions::TooDeepParameters => e
15
+ # ...
16
+
17
+ # After
18
+ rescue Grape::Exceptions::RequestError => e
19
+ # ...
20
+ ```
21
+
22
+ The error message is now forwarded directly from Rack rather than translated through Grape's locale system. On Rack 3, all Rack bad-request errors share the `Rack::BadRequest` marker module and are covered by a single rescue.
23
+
24
+ #### `endpoint_run_validators.grape` notification no longer fired when there are no validators
25
+
26
+ `ActiveSupport::Notifications` subscribers listening to `endpoint_run_validators.grape` will no longer receive an event for endpoints that have no validators. If you rely on this notification to measure every request, subscribe to `endpoint_run.grape` instead, which always fires.
27
+
28
+ #### Custom validators: use `default_message_key` and `validation_error!`
29
+
30
+ Validators are now instantiated once at definition time and frozen. Any setup should happen in `initialize`, not in `validate_param!`.
31
+
32
+ If your custom validator did work in `validate_param!` that only depends on the validator's options (not the param value), move it to `initialize`. A common case is compiling a value derived from options — for example, building a `Regexp`. Previously this may have been cached back into `@options`, which now raises `FrozenError` since `@options` and its nested values are deep-frozen by the base class:
33
+
34
+ **Before:**
35
+ ```ruby
36
+ class MyValidator < Grape::Validations::Validators::Base
37
+ def validate_param!(attr_name, params)
38
+ # raises FrozenError: @options is frozen, cannot store compiled pattern back into it
39
+ @options[:compiled] ||= Regexp.new(@options[:pattern])
40
+ validation_error!(attr_name) unless params[attr_name].match?(@options[:compiled])
41
+ end
42
+ end
43
+ ```
44
+
45
+ **After:**
46
+ ```ruby
47
+ class MyValidator < Grape::Validations::Validators::Base
48
+ def initialize(attrs, options, required, scope, opts)
49
+ super
50
+ @pattern = Regexp.new(@options[:pattern]).freeze
51
+ end
52
+
53
+ def validate_param!(attr_name, params)
54
+ validation_error!(attr_name) unless params[attr_name].match?(@pattern)
55
+ end
56
+ end
57
+ ```
58
+
59
+ Any Array or Hash derived from options and stored in an ivar should be frozen, since the validator instance is shared across requests. `@options` itself (and any nested Hash/Array/String values within it) is deep-frozen by the base class, so mutations like `@options[:values] << 'extra'` will also raise a `FrozenError`.
60
+
61
+ #### Custom validators: rename `@option` to `@options`
62
+
63
+ The instance variable holding the validator's option value has been renamed from `@option` to `@options`. `@option` remains as an alias for backwards compatibility but will be removed in the next major release. Update any custom validators to use `@options` instead.
64
+
65
+ Several new helpers are available — see [Available helpers](README.md#available-helpers) in the README for full documentation and examples.
66
+
67
+ #### `with` now uses keyword arguments
68
+
69
+ The `with` DSL method now uses `**opts` instead of a positional hash. Calls using bare keyword syntax are unaffected:
70
+
71
+ ```ruby
72
+ # still works
73
+ with(type: String, documentation: { in: 'body' }) { ... }
74
+ ```
75
+
76
+ However, passing an explicit hash literal will now raise an `ArgumentError`:
77
+
78
+ ```ruby
79
+ # raises ArgumentError
80
+ with({ type: String }) { ... }
81
+ ```
82
+
83
+ See [#2663](https://github.com/ruby-grape/grape/pull/2663) for more information.
84
+
85
+ #### Custom validators: use `translate` instead of `I18n` directly
86
+
87
+ `Grape::Util::Translation` is now included in `Grape::Validations::Validators::Base`. Custom validators that previously called `I18n.t` or `I18n.translate` directly should switch to the `translate`, which provides the same `:en` fallback logic used by all built-in validators.
88
+
89
+ Key points:
90
+ - `scope` defaults to `'grape.errors.messages'` — no need to specify it for standard error message keys.
91
+ - Interpolation variables are passed directly to I18n.
92
+ - `format` is no longer needed — `translate` returns the fully interpolated string.
93
+
94
+ ```ruby
95
+ # Before
96
+ raise Grape::Exceptions::Validation.new(
97
+ params: [@scope.full_name(attr_name)],
98
+ message: format(I18n.t(:my_key, scope: 'grape.errors.messages'), min: 2, max: 10)
99
+ )
100
+
101
+ # After
102
+ raise Grape::Exceptions::Validation.new(
103
+ params: [@scope.full_name(attr_name)],
104
+ message: translate(:my_key, min: 2, max: 10)
105
+ )
106
+ ```
107
+
108
+ See [#2662](https://github.com/ruby-grape/grape/pull/2662) for more information.
109
+
110
+ ### Upgrading to >= 3.1
111
+
112
+ #### Explicit kwargs for `namespace` and `route_param`
113
+
114
+ 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.
115
+
116
+ #### ParamsBuilder Grape::Extensions
117
+
118
+ Deprecated [ParamsBuilder's extensions](https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#params-builder) have been removed.
119
+
120
+ #### Enhanced API compile!
121
+
122
+ 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.
123
+
124
+ ### Upgrading to >= 3.0.0
125
+
126
+ #### Ruby 3+ Argument Delegation Modernization
127
+
128
+ Grape has been modernized to use Ruby 3+'s preferred argument delegation patterns. This change replaces `args.extract_options!` with explicit `**kwargs` parameters throughout the codebase.
129
+
130
+ - All DSL methods now use explicit keyword arguments (`**kwargs`) instead of extracting options from mixed argument lists
131
+ - Method signatures are now more explicit and follow Ruby 3+ best practices
132
+ - The `active_support/core_ext/array/extract_options` dependency has been removed
133
+
134
+ This is a modernization effort that improves code quality while maintaining full backward compatibility.
135
+
136
+ See [#2618](https://github.com/ruby-grape/grape/pull/2618) for more information.
137
+
138
+ #### Configuration API Migration from ActiveSupport::Configurable to Dry::Configurable
139
+
140
+ Grape has migrated from `ActiveSupport::Configurable` to `Dry::Configurable` for its configuration system since its [deprecated](https://github.com/rails/rails/blob/1cdd190a25e483b65f1f25bbd0f13a25d696b461/activesupport/lib/active_support/configurable.rb#L3-L7).
141
+
142
+ See [#2617](https://github.com/ruby-grape/grape/pull/2617) for more information.
143
+
144
+ #### Endpoint execution simplified and `return` deprecated
145
+
146
+ Executing a endpoint's block has been simplified and calling `return` in it has been deprecated. Use `next` instead.
147
+
148
+ See [#2577](https://github.com/ruby-grape/grape/pull/2577) for more information.
149
+
150
+ #### Old Deprecations Clean Up
151
+
152
+ - `rack_response` has been removed in favor of using `error!`.
153
+ - `Grape::Exceptions::MissingGroupType` and `Grape::Exceptions::UnsupportedGroupType` aliases `MissingGroupTypeError and `UnsupportedGroupType` have been removed.
154
+ - `Grape::Validations::Base` has been removed in favor of `Grape::Validations::Validators::Base`.
155
+
156
+ See [2573](https://github.com/ruby-grape/grape/pull/2573) for more information.
157
+
4
158
  ### Upgrading to >= 2.4.0
5
159
 
6
160
  #### Grape::Middleware::Auth::Base
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,7 +20,8 @@ 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'
25
25
  s.add_dependency 'dry-types', '>= 1.1'
26
26
  s.add_dependency 'mustermann-grape', '~> 1.1.0'
27
27
  s.add_dependency 'rack', '>= 2'
@@ -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 = '>= 2.7.0'
32
+ s.required_ruby_version = '>= 3.2'
33
33
  end
@@ -5,32 +5,33 @@ 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
+ extend Grape::DSL::Settings
9
+ extend Grape::DSL::Desc
10
+ extend Grape::DSL::Validations
11
+ extend Grape::DSL::Callbacks
12
+ extend Grape::DSL::Logger
13
+ extend Grape::DSL::Middleware
14
+ extend Grape::DSL::RequestResponse
15
+ extend Grape::DSL::Routing
16
+ extend Grape::DSL::Helpers
8
17
  extend Grape::Middleware::Auth::DSL
9
- include Grape::DSL::API
18
+
19
+ Boolean = Grape::API::Boolean
10
20
 
11
21
  class << self
12
- attr_reader :instance, :base
22
+ extend Forwardable
23
+
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
17
- end
18
-
19
- def mounted(&block)
20
- evaluate_as_instance_with_configuration(block, lazy: true)
21
- end
26
+ def_delegators :@base, :to_s
22
27
 
23
28
  def base=(grape_api)
24
29
  @base = grape_api
25
30
  grape_api.instances << self
26
31
  end
27
32
 
28
- def to_s
29
- base&.to_s || super
30
- end
31
-
32
33
  def base_instance?
33
- self == base.base_instance
34
+ self == @base.base_instance
34
35
  end
35
36
 
36
37
  # A class-level lock to ensure the API is not compiled by multiple
@@ -44,92 +45,33 @@ module Grape
44
45
  reset_validations!
45
46
  end
46
47
 
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
48
  # This is the interface point between Rack and Grape; it accepts a request
59
49
  # from Rack and ultimately returns an array of three values: the status,
60
50
  # the headers, and the body. See [the rack specification]
61
51
  # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
62
52
  def call(env)
63
53
  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
54
+ @instance.call(env)
79
55
  end
80
56
 
81
57
  def compile!
82
- return if instance
58
+ return if @instance
83
59
 
84
- LOCK.synchronize { compile unless instance }
60
+ LOCK.synchronize { @instance ||= new }
85
61
  end
86
62
 
87
63
  # see Grape::Router#recognize_path
88
64
  def recognize_path(path)
89
65
  compile!
90
- instance.router.recognize_path(path)
66
+ @instance.router.recognize_path(path)
91
67
  end
92
68
 
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
111
- end
112
-
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
69
+ # Wipe the compiled API so we can recompile after changes were made.
70
+ def change!
71
+ @instance = nil
126
72
  end
127
73
 
128
- def inherited(subclass)
129
- super
130
- subclass.reset!
131
- subclass.logger = logger.clone
132
- end
74
+ protected
133
75
 
134
76
  def inherit_settings(other_settings)
135
77
  top_level_setting.inherit_from other_settings.point_in_time_copy
@@ -143,6 +85,14 @@ module Grape
143
85
 
144
86
  reset_routes!
145
87
  end
88
+
89
+ private
90
+
91
+ def inherited(subclass)
92
+ super
93
+ subclass.reset!
94
+ subclass.logger logger.clone
95
+ end
146
96
  end
147
97
 
148
98
  attr_reader :router
@@ -180,8 +130,9 @@ module Grape
180
130
  # errors from reaching upstream. This is effectivelly done by unsetting
181
131
  # X-Cascade. Default :cascade is true.
182
132
  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)
133
+ namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
134
+ return namespace_inheritable[:cascade] if namespace_inheritable.key?(:cascade)
135
+ return namespace_inheritable[:version_options][:cascade] if namespace_inheritable[:version_options]&.key?(:cascade)
185
136
 
186
137
  true
187
138
  end
@@ -197,7 +148,7 @@ module Grape
197
148
  def add_head_not_allowed_methods_and_options_methods
198
149
  # The paths we collected are prepared (cf. Path#prepare), so they
199
150
  # contain already versioning information when using path versioning.
200
- all_routes = self.class.endpoints.map(&:routes).flatten
151
+ all_routes = self.class.endpoints.flat_map(&:routes)
201
152
 
202
153
  # Disable versioning so adding a route won't prepend versioning
203
154
  # informations again.
@@ -206,41 +157,37 @@ module Grape
206
157
 
207
158
  def collect_route_config_per_pattern(all_routes)
208
159
  routes_by_regexp = all_routes.group_by(&:pattern_regexp)
160
+ namespace_inheritable = self.class.inheritable_setting.namespace_inheritable
209
161
 
210
162
  # Build the configuration based on the first endpoint and the collection of methods supported.
211
163
  routes_by_regexp.each_value do |routes|
212
- last_route = routes.last # Most of the configuration is taken from the last endpoint
213
164
  next if routes.any? { |route| route.request_method == '*' }
214
165
 
166
+ last_route = routes.last # Most of the configuration is taken from the last endpoint
215
167
  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)
168
+ allowed_methods |= [Rack::HEAD] if !namespace_inheritable[:do_not_route_head] && allowed_methods.include?(Rack::GET)
217
169
 
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)
170
+ allow_header = namespace_inheritable[:do_not_route_options] ? allowed_methods : [Rack::OPTIONS] | allowed_methods
171
+ last_route.app.options[:options_route_enabled] = true unless namespace_inheritable[:do_not_route_options] || allowed_methods.include?(Rack::OPTIONS)
220
172
 
221
- @router.associate_routes(last_route.pattern, {
222
- endpoint: last_route.app,
223
- allow_header: allow_header
224
- })
173
+ greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:)
174
+ @router.associate_routes(greedy_route)
225
175
  end
226
176
  end
227
177
 
178
+ ROOT_PREFIX_VERSIONING_KEY = %i[version version_options root_prefix].freeze
179
+ private_constant :ROOT_PREFIX_VERSIONING_KEY
180
+
228
181
  # Allows definition of endpoints that ignore the versioning configuration
229
182
  # used by the rest of your API.
230
183
  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)
234
-
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
-
184
+ inheritable_setting = self.class.inheritable_setting
185
+ deleted_values = inheritable_setting.namespace_inheritable.delete(*ROOT_PREFIX_VERSIONING_KEY)
239
186
  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)
187
+ ensure
188
+ ROOT_PREFIX_VERSIONING_KEY.each_with_index do |key, index|
189
+ inheritable_setting.namespace_inheritable[key] = deleted_values[index]
190
+ end
244
191
  end
245
192
  end
246
193
  end
data/lib/grape/api.rb CHANGED
@@ -5,7 +5,9 @@ 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].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
+
10
+ Helpers = Grape::DSL::Helpers::BaseHelper
9
11
 
10
12
  class Boolean
11
13
  def self.build(val)
@@ -15,32 +17,19 @@ module Grape
15
17
  end
16
18
  end
17
19
 
18
- class Instance
19
- Boolean = Grape::API::Boolean
20
- end
21
-
22
20
  class << self
23
21
  extend Forwardable
22
+
24
23
  attr_accessor :base_instance, :instances
25
24
 
26
25
  delegate_missing_to :base_instance
27
- def_delegators :base_instance, :new, :configuration
28
26
 
29
27
  # This is the interface point between Rack and Grape; it accepts a request
30
28
  # from Rack and ultimately returns an array of three values: the status,
31
29
  # the headers, and the body. See [the rack specification]
32
- # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
30
+ # (https://github.com/rack/rack/blob/main/SPEC.rdoc) for more.
33
31
  # 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
32
+ def_delegators :base_instance, :new, :configuration, :call, :change!, :compile!, :recognize_path, :routes
44
33
 
45
34
  # Initialize the instance variables on the remountable class, and the base_instance
46
35
  # an instance that will be used to create the set up but will not be mounted
@@ -54,8 +43,8 @@ module Grape
54
43
  # Redefines all methods so that are forwarded to add_setup and be recorded
55
44
  def override_all_methods!
56
45
  (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)
46
+ define_singleton_method(method_override) do |*args, **kwargs, &block|
47
+ add_setup(method: method_override, args:, kwargs:, block:)
59
48
  end
60
49
  end
61
50
  end
@@ -89,6 +78,15 @@ module Grape
89
78
 
90
79
  private
91
80
 
81
+ # When inherited, will create a list of all instances (times the API was mounted)
82
+ # It will listen to the setup required to mount that endpoint, and replicate it on any new instance
83
+ def inherited(api)
84
+ super
85
+
86
+ api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
87
+ api.override_all_methods!
88
+ end
89
+
92
90
  # Replays the set up to produce an API as defined in this class, can be called
93
91
  # on classes that inherit from Grape::API
94
92
  def replay_setup_on(instance)
@@ -97,16 +95,8 @@ module Grape
97
95
  end
98
96
  end
99
97
 
100
- def instance_for_rack
101
- if never_mounted?
102
- base_instance
103
- else
104
- mounted_instances.first
105
- end
106
- end
107
-
108
98
  # Adds a new stage to the set up require to get a Grape::API up and running
109
- def add_setup(step)
99
+ def add_setup(**step)
110
100
  @setup << step
111
101
  last_response = nil
112
102
  @instances.each do |instance|
@@ -130,12 +120,13 @@ module Grape
130
120
  end
131
121
  end
132
122
 
133
- def replay_step_on(instance, method:, args:, block:)
134
- return if skip_immediate_run?(instance, args)
123
+ def replay_step_on(instance, method:, args:, kwargs:, block:)
124
+ return if skip_immediate_run?(instance, args, kwargs)
135
125
 
136
126
  eval_args = evaluate_arguments(instance.configuration, *args)
137
- response = instance.send(method, *eval_args, &block)
138
- if skip_immediate_run?(instance, [response])
127
+ eval_kwargs = kwargs.deep_transform_values { |v| evaluate_arguments(instance.configuration, v).first }
128
+ response = instance.__send__(method, *eval_args, **eval_kwargs, &block)
129
+ if skip_immediate_run?(instance, [response], kwargs)
139
130
  response
140
131
  else
141
132
  evaluate_arguments(instance.configuration, response).first
@@ -143,18 +134,18 @@ module Grape
143
134
  end
144
135
 
145
136
  # Skips steps that contain arguments to be lazily executed (on re-mount time)
146
- def skip_immediate_run?(instance, args)
137
+ def skip_immediate_run?(instance, args, kwargs)
147
138
  instance.base_instance? &&
148
- (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) })
139
+ (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) } || any_lazy?(kwargs.values))
149
140
  end
150
141
 
151
142
  def any_lazy?(args)
152
- args.any? { |argument| argument.try(:lazy?) }
143
+ args.any? { |argument| argument_lazy?(argument) }
153
144
  end
154
145
 
155
146
  def evaluate_arguments(configuration, *args)
156
147
  args.map do |argument|
157
- if argument.try(:lazy?)
148
+ if argument_lazy?(argument)
158
149
  argument.evaluate_from(configuration)
159
150
  elsif argument.is_a?(Hash)
160
151
  argument.transform_values { |value| evaluate_arguments(configuration, value).first }
@@ -166,12 +157,8 @@ module Grape
166
157
  end
167
158
  end
168
159
 
169
- def never_mounted?
170
- mounted_instances.empty?
171
- end
172
-
173
- def mounted_instances
174
- instances - [base_instance]
160
+ def argument_lazy?(argument)
161
+ argument.respond_to?(:lazy?) && argument.lazy?
175
162
  end
176
163
  end
177
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