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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +351 -0
- data/CONTRIBUTING.md +18 -10
- data/README.md +457 -228
- data/UPGRADING.md +1240 -1
- data/grape.gemspec +7 -7
- data/lib/grape/api/instance.rb +65 -116
- data/lib/grape/api.rb +53 -62
- data/lib/grape/content_types.rb +58 -3
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +116 -0
- data/lib/grape/dry_types.rb +48 -4
- data/lib/grape/dsl/callbacks.rb +8 -58
- data/lib/grape/dsl/declared.rb +35 -0
- data/lib/grape/dsl/desc.rb +24 -67
- data/lib/grape/dsl/entity.rb +106 -0
- data/lib/grape/dsl/helpers.rb +80 -65
- data/lib/grape/dsl/inside_route.rb +56 -296
- data/lib/grape/dsl/logger.rb +4 -9
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +86 -93
- data/lib/grape/dsl/request_response.rb +139 -139
- data/lib/grape/dsl/rescue_options.rb +25 -0
- data/lib/grape/dsl/routing.rb +328 -203
- data/lib/grape/dsl/settings.rb +29 -135
- data/lib/grape/dsl/validations.rb +38 -44
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +25 -0
- data/lib/grape/endpoint.rb +285 -245
- data/lib/grape/env.rb +8 -3
- data/lib/grape/error_formatter/base.rb +27 -21
- data/lib/grape/error_formatter/json.rb +6 -0
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/base.rb +20 -44
- data/lib/grape/exceptions/error_response.rb +48 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_message_body.rb +1 -1
- data/lib/grape/exceptions/invalid_version_header.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/method_not_allowed.rb +1 -1
- data/lib/grape/exceptions/missing_group_type.rb +0 -2
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/request_error.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/unknown_parameter.rb +1 -1
- data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
- data/lib/grape/exceptions/validation.rb +25 -7
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +21 -23
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +6 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +3 -5
- data/lib/grape/middleware/auth/base.rb +4 -3
- data/lib/grape/middleware/auth/dsl.rb +9 -23
- data/lib/grape/middleware/base.rb +41 -36
- data/lib/grape/middleware/error.rb +271 -73
- data/lib/grape/middleware/formatter.rb +152 -63
- data/lib/grape/middleware/precomputed_content_types.rb +51 -0
- data/lib/grape/middleware/stack.rb +20 -11
- data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
- data/lib/grape/middleware/versioner/base.rb +40 -48
- data/lib/grape/middleware/versioner/header.rb +5 -23
- data/lib/grape/middleware/versioner/param.rb +2 -2
- data/lib/grape/middleware/versioner/path.rb +54 -7
- data/lib/grape/mountable.rb +22 -0
- data/lib/grape/namespace.rb +19 -11
- data/lib/grape/params_builder/base.rb +2 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/params_builder.rb +2 -19
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +8 -72
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +57 -20
- data/lib/grape/router/base_route.rb +53 -7
- data/lib/grape/router/greedy_route.rb +16 -6
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +65 -37
- data/lib/grape/router/route.rb +66 -18
- data/lib/grape/router.rb +126 -115
- data/lib/grape/serve_stream/file_body.rb +7 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +7 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/api_description.rb +67 -0
- data/lib/grape/util/cache.rb +22 -2
- data/lib/grape/util/deep_freeze.rb +34 -0
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +777 -39
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +13 -9
- data/lib/grape/util/path_normalizer.rb +37 -0
- data/lib/grape/util/registry.rb +13 -3
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +39 -10
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +61 -29
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/contract_scope.rb +8 -13
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/oneof_collector.rb +35 -0
- data/lib/grape/validations/param_scope_tracker.rb +62 -0
- data/lib/grape/validations/params_documentation.rb +52 -0
- data/lib/grape/validations/params_scope.rb +217 -304
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/single_attribute_iterator.rb +6 -4
- data/lib/grape/validations/types/array_coercer.rb +7 -10
- data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +14 -33
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
- data/lib/grape/validations/types.rb +29 -51
- data/lib/grape/validations/validations_spec.rb +164 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
- data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
- data/lib/grape/validations/validators/base.rb +118 -34
- data/lib/grape/validations/validators/coerce_validator.rb +25 -37
- data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
- data/lib/grape/validations/validators/default_validator.rb +13 -20
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +14 -4
- data/lib/grape/validations/validators/length_validator.rb +23 -22
- data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
- data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +51 -0
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +11 -3
- data/lib/grape/validations/validators/same_as_validator.rb +7 -15
- data/lib/grape/validations/validators/values_validator.rb +34 -26
- data/lib/grape/version.rb +1 -2
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +58 -31
- metadata +62 -39
- data/lib/grape/api/helpers.rb +0 -9
- data/lib/grape/dsl/api.rb +0 -17
- data/lib/grape/dsl/configuration.rb +0 -15
- data/lib/grape/exceptions/conflicting_types.rb +0 -11
- data/lib/grape/exceptions/empty_message_body.rb +0 -11
- data/lib/grape/exceptions/invalid_parameters.rb +0 -11
- data/lib/grape/exceptions/missing_option.rb +0 -11
- data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
- data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
- data/lib/grape/exceptions/unknown_options.rb +0 -11
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
- data/lib/grape/extensions/hash.rb +0 -27
- data/lib/grape/extensions/hashie/mash.rb +0 -24
- data/lib/grape/middleware/globals.rb +0 -14
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -29
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
- data/lib/grape/util/strict_hash_configuration.rb +0 -108
- data/lib/grape/validations/attributes_doc.rb +0 -60
- data/lib/grape/validations/validator_factory.rb +0 -15
data/lib/grape/endpoint.rb
CHANGED
|
@@ -8,138 +8,92 @@ module Grape
|
|
|
8
8
|
class Endpoint
|
|
9
9
|
extend Forwardable
|
|
10
10
|
include Grape::DSL::Settings
|
|
11
|
+
include Grape::DSL::Headers
|
|
11
12
|
include Grape::DSL::InsideRoute
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
attr_reader :env, :request, :source, :options, :endpoints
|
|
15
|
+
attr_accessor :options_route_enabled
|
|
15
16
|
|
|
16
17
|
def_delegators :request, :params, :headers, :cookies
|
|
17
18
|
def_delegator :cookies, :response_cookies
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
self == Endpoint ? Class.new(Endpoint).new(...) : super
|
|
22
|
-
end
|
|
20
|
+
# The API (a +Grape::API+ instance) this endpoint belongs to.
|
|
21
|
+
def_delegator :@config, :api
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
# The logger configured on the API this endpoint belongs to. Available
|
|
24
|
+
# inside route handlers, +before+/+after+/+after_validation+/+finally+
|
|
25
|
+
# filters, and +rescue_from+ blocks.
|
|
26
|
+
def_delegator :api, :logger
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
# The Rack app or Grape API mounted at this endpoint, or +nil+ for a plain
|
|
29
|
+
# block endpoint. Prefer this over +options[:app]+, which is retained only
|
|
30
|
+
# for backwards compatibility.
|
|
31
|
+
def mounted_app
|
|
32
|
+
config.app
|
|
33
|
+
end
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
end
|
|
35
|
+
class << self
|
|
36
|
+
def block_to_unbound_method(block)
|
|
37
|
+
return unless block
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
# The unbound method allows explicit calls to +return+ without raising a
|
|
46
|
-
# +LocalJumpError+. The method will be removed, but a +Proc+ reference to
|
|
47
|
-
# it will be returned. The returned +Proc+ expects a single argument: the
|
|
48
|
-
# instance of +Endpoint+ to bind to the method during the call.
|
|
49
|
-
#
|
|
50
|
-
# @param [String, Symbol] method_name
|
|
51
|
-
# @return [Proc]
|
|
52
|
-
# @raise [NameError] an instance method with the same name already exists
|
|
53
|
-
def generate_api_method(method_name, &block)
|
|
54
|
-
raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") if method_defined?(method_name)
|
|
55
|
-
|
|
56
|
-
define_method(method_name, &block)
|
|
57
|
-
method = instance_method(method_name)
|
|
58
|
-
remove_method(method_name)
|
|
59
|
-
|
|
60
|
-
proc do |endpoint_instance|
|
|
61
|
-
ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
|
|
62
|
-
method.bind_call(endpoint_instance)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
39
|
+
define_method :temp_unbound_method, block
|
|
40
|
+
method = instance_method(:temp_unbound_method)
|
|
41
|
+
remove_method :temp_unbound_method
|
|
42
|
+
method
|
|
65
43
|
end
|
|
66
44
|
end
|
|
67
45
|
|
|
68
46
|
# Create a new endpoint.
|
|
69
47
|
# @param new_settings [InheritableSetting] settings to determine the params,
|
|
70
48
|
# validations, and other properties from.
|
|
71
|
-
# @param
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
49
|
+
# @param http_methods [String or Array] which HTTP method(s) can be used to
|
|
50
|
+
# reach this endpoint.
|
|
51
|
+
# @param path [String or Array] the path to this endpoint, within the
|
|
52
|
+
# current scope.
|
|
53
|
+
# @param api [Grape::API] the API this endpoint belongs to. Exposed as
|
|
54
|
+
# {#api}.
|
|
55
|
+
# @param app [#call, nil] the Rack app or Grape API mounted at this
|
|
56
|
+
# endpoint; +nil+ for a plain block endpoint. Exposed as {#mounted_app}.
|
|
57
|
+
# @param params [Hash] the declared params for this endpoint, keyed by name.
|
|
58
|
+
# Kept out of +route_options+ and read via +config.params+.
|
|
59
|
+
# @param requirements [Hash, nil] regular-expression constraints for named
|
|
60
|
+
# path params. Read via +config.requirements+.
|
|
61
|
+
# @param anchor [Boolean] whether the route anchors to the whole path
|
|
62
|
+
# (default +true+). Read via +config.anchor+.
|
|
63
|
+
# @param options [Hash] attributes of this endpoint, normalized into a
|
|
64
|
+
# +Grape::Endpoint::Options+ value object.
|
|
76
65
|
# @option options route_options [Hash]
|
|
77
66
|
# @note This happens at the time of API definition, so in this context the
|
|
78
67
|
# endpoint does not know if it will be mounted under a different endpoint.
|
|
79
68
|
# @yield a block defining what your API should do when this endpoint is hit
|
|
80
|
-
def initialize(new_settings,
|
|
81
|
-
|
|
82
|
-
require_option(options, :method)
|
|
83
|
-
|
|
84
|
-
self.inheritable_setting = new_settings.point_in_time_copy
|
|
85
|
-
|
|
86
|
-
# now +namespace_stackable(:declared_params)+ contains all params defined for
|
|
87
|
-
# this endpoint and its parents, but later it will be cleaned up,
|
|
88
|
-
# see +reset_validations!+ in lib/grape/dsl/validations.rb
|
|
89
|
-
route_setting(:declared_params, namespace_stackable(:declared_params).flatten)
|
|
90
|
-
route_setting(:saved_validations, namespace_stackable(:validations))
|
|
91
|
-
|
|
92
|
-
namespace_stackable(:representations, []) unless namespace_stackable(:representations)
|
|
93
|
-
namespace_inheritable(:default_error_status, 500) unless namespace_inheritable(:default_error_status)
|
|
69
|
+
def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, requirements: nil, anchor: true, **options, &block)
|
|
70
|
+
self.inheritable_setting = new_settings.point_in_time_copy_for_endpoint
|
|
94
71
|
|
|
95
72
|
@options = options
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@options[:method] = Array(options[:method])
|
|
101
|
-
@options[:route_options] ||= {}
|
|
102
|
-
|
|
103
|
-
@lazy_initialize_lock = Mutex.new
|
|
104
|
-
@lazy_initialized = nil
|
|
105
|
-
@block = nil
|
|
73
|
+
@config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
|
|
74
|
+
# +:app+ is still surfaced on the public options Hash for backwards
|
|
75
|
+
# compatibility (e.g. grape-swagger); prefer the +mounted_app+ reader.
|
|
76
|
+
@options[:app] = app if app
|
|
106
77
|
|
|
107
78
|
@status = nil
|
|
108
79
|
@stream = nil
|
|
109
80
|
@body = nil
|
|
110
|
-
@
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@source = block
|
|
115
|
-
@block = self.class.generate_api_method(method_name, &block)
|
|
81
|
+
@source = self.class.block_to_unbound_method(block)
|
|
82
|
+
@before_filter_passed = false
|
|
83
|
+
@options_route_enabled = false
|
|
84
|
+
@endpoints = @config.app.endpoints if @config.app.respond_to?(:endpoints)
|
|
116
85
|
end
|
|
117
86
|
|
|
118
|
-
# Update our settings from a given
|
|
87
|
+
# Update our settings from a given parent settings instance. Used when
|
|
119
88
|
# the endpoint's API is mounted under another one.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
inheritable_setting.
|
|
123
|
-
|
|
124
|
-
inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any?
|
|
125
|
-
|
|
126
|
-
endpoints&.each { |e| e.inherit_settings(namespace_stackable) }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def require_option(options, key)
|
|
130
|
-
raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def method_name
|
|
134
|
-
[options[:method],
|
|
135
|
-
Namespace.joined_space(namespace_stackable(:namespace)),
|
|
136
|
-
(namespace_stackable(:mount_path) || []).join('/'),
|
|
137
|
-
options[:path].join('/')]
|
|
138
|
-
.join(' ')
|
|
89
|
+
# @param settings [Grape::Util::InheritableSetting]
|
|
90
|
+
def inherit_settings(settings)
|
|
91
|
+
inheritable_setting.inherit_route_params(settings)
|
|
92
|
+
endpoints&.each { |e| e.inherit_settings(settings) }
|
|
139
93
|
end
|
|
140
94
|
|
|
141
95
|
def routes
|
|
142
|
-
@routes ||= endpoints&.
|
|
96
|
+
@routes ||= endpoints&.flat_map(&:routes) || to_routes
|
|
143
97
|
end
|
|
144
98
|
|
|
145
99
|
def reset_routes!
|
|
@@ -149,74 +103,27 @@ module Grape
|
|
|
149
103
|
end
|
|
150
104
|
|
|
151
105
|
def mount_in(router)
|
|
152
|
-
|
|
106
|
+
if endpoints
|
|
107
|
+
compile!
|
|
108
|
+
return endpoints.each { |e| e.mount_in(router) }
|
|
109
|
+
end
|
|
153
110
|
|
|
154
111
|
reset_routes!
|
|
112
|
+
compile!
|
|
155
113
|
routes.each do |route|
|
|
156
114
|
router.append(route.apply(self))
|
|
157
|
-
next
|
|
115
|
+
next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
|
|
158
116
|
|
|
159
|
-
route.
|
|
160
|
-
head_route.convert_to_head_request!
|
|
161
|
-
router.append(head_route.apply(self))
|
|
162
|
-
end
|
|
117
|
+
router.append(route.to_head.apply(self))
|
|
163
118
|
end
|
|
164
119
|
end
|
|
165
120
|
|
|
166
|
-
def to_routes
|
|
167
|
-
default_route_options = prepare_default_route_attributes
|
|
168
|
-
|
|
169
|
-
map_routes do |method, raw_path|
|
|
170
|
-
prepared_path = Path.new(raw_path, namespace, prepare_default_path_settings)
|
|
171
|
-
params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
|
|
172
|
-
route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
|
|
173
|
-
route.apply(self)
|
|
174
|
-
end.flatten
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def prepare_routes_requirements
|
|
178
|
-
{}.merge!(*namespace_stackable(:namespace).map(&:requirements)).tap do |requirements|
|
|
179
|
-
endpoint_requirements = options.dig(:route_options, :requirements)
|
|
180
|
-
requirements.merge!(endpoint_requirements) if endpoint_requirements
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def prepare_default_route_attributes
|
|
185
|
-
{
|
|
186
|
-
namespace: namespace,
|
|
187
|
-
version: prepare_version,
|
|
188
|
-
requirements: prepare_routes_requirements,
|
|
189
|
-
prefix: namespace_inheritable(:root_prefix),
|
|
190
|
-
anchor: options[:route_options].fetch(:anchor, true),
|
|
191
|
-
settings: inheritable_setting.route.except(:declared_params, :saved_validations),
|
|
192
|
-
forward_match: options[:forward_match]
|
|
193
|
-
}
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def prepare_version
|
|
197
|
-
version = namespace_inheritable(:version)
|
|
198
|
-
return if version.blank?
|
|
199
|
-
|
|
200
|
-
version.length == 1 ? version.first : version
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def map_routes
|
|
204
|
-
options[:method].map { |method| options[:path].map { |path| yield method, path } }
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def prepare_default_path_settings
|
|
208
|
-
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
|
|
209
|
-
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
|
|
210
|
-
namespace_stackable_hash.merge!(namespace_inheritable_hash)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
121
|
def namespace
|
|
214
|
-
@namespace ||=
|
|
122
|
+
@namespace ||= inheritable_setting.namespace_path
|
|
215
123
|
end
|
|
216
124
|
|
|
217
125
|
def call(env)
|
|
218
|
-
|
|
219
|
-
dup.call!(env)
|
|
126
|
+
@prototype.dup.call!(env)
|
|
220
127
|
end
|
|
221
128
|
|
|
222
129
|
def call!(env)
|
|
@@ -225,14 +132,19 @@ module Grape
|
|
|
225
132
|
@app.call(env)
|
|
226
133
|
end
|
|
227
134
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
135
|
+
def ==(other)
|
|
136
|
+
other.is_a?(self.class) &&
|
|
137
|
+
config == other.config &&
|
|
138
|
+
inheritable_setting == other.inheritable_setting
|
|
232
139
|
end
|
|
140
|
+
alias eql? ==
|
|
233
141
|
|
|
234
|
-
|
|
235
|
-
|
|
142
|
+
# Mirrors #==. The class stays out: #== admits a subclass instance
|
|
143
|
+
# through is_a?, and such a pair must hash alike. The block (#source) is
|
|
144
|
+
# not part of either, matching the long-standing duplicate-route check in
|
|
145
|
+
# DSL::Routing#route.
|
|
146
|
+
def hash
|
|
147
|
+
[config, inheritable_setting].hash
|
|
236
148
|
end
|
|
237
149
|
|
|
238
150
|
# The purpose of this override is solely for stripping internals when an error occurs while calling
|
|
@@ -247,22 +159,21 @@ module Grape
|
|
|
247
159
|
protected
|
|
248
160
|
|
|
249
161
|
def run
|
|
250
|
-
|
|
251
|
-
@request = Grape::Request.new(env, build_params_with:
|
|
162
|
+
instrument_run do
|
|
163
|
+
@request = Grape::Request.new(env, build_params_with: @build_params_with)
|
|
252
164
|
begin
|
|
253
|
-
self.class.run_before_each(self)
|
|
254
165
|
run_filters befores, :before
|
|
166
|
+
@before_filter_passed = true
|
|
255
167
|
|
|
256
168
|
if env.key?(Grape::Env::GRAPE_ALLOWED_METHODS)
|
|
257
169
|
header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
|
|
258
170
|
raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?
|
|
259
171
|
|
|
260
|
-
header 'Allow', header['Allow']
|
|
261
172
|
response_object = ''
|
|
262
173
|
status 204
|
|
263
174
|
else
|
|
264
175
|
run_filters before_validations, :before_validation
|
|
265
|
-
run_validators
|
|
176
|
+
run_validators(request:)
|
|
266
177
|
run_filters after_validations, :after_validation
|
|
267
178
|
response_object = execute
|
|
268
179
|
end
|
|
@@ -284,134 +195,263 @@ module Grape
|
|
|
284
195
|
end
|
|
285
196
|
|
|
286
197
|
def execute
|
|
287
|
-
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def helpers
|
|
291
|
-
lazy_initialize! && @helpers
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def lazy_initialize!
|
|
295
|
-
return true if @lazy_initialized
|
|
296
|
-
|
|
297
|
-
@lazy_initialize_lock.synchronize do
|
|
298
|
-
return true if @lazy_initialized
|
|
299
|
-
|
|
300
|
-
@helpers = build_helpers&.tap { |mod| self.class.include mod }
|
|
301
|
-
@app = options[:app] || build_stack(@helpers)
|
|
198
|
+
return unless source
|
|
302
199
|
|
|
303
|
-
|
|
200
|
+
instrument_render do
|
|
201
|
+
source.bind_call(self)
|
|
304
202
|
end
|
|
305
203
|
end
|
|
306
204
|
|
|
307
|
-
def run_validators(
|
|
308
|
-
|
|
205
|
+
def run_validators(request:)
|
|
206
|
+
validators = inheritable_setting.route_validations
|
|
207
|
+
return if validators.blank?
|
|
309
208
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
209
|
+
validation_exceptions = nil
|
|
210
|
+
|
|
211
|
+
Grape::Validations::ParamScopeTracker.track do
|
|
212
|
+
instrument_run_validators(validators, request) do
|
|
213
|
+
validators.each do |validator|
|
|
214
|
+
validator.validate(request)
|
|
215
|
+
rescue Grape::Exceptions::Validation, Grape::Exceptions::ValidationArrayErrors => e
|
|
216
|
+
(validation_exceptions ||= []) << e
|
|
217
|
+
break if validator.fail_fast?
|
|
218
|
+
end
|
|
319
219
|
end
|
|
320
220
|
end
|
|
321
221
|
|
|
322
|
-
|
|
222
|
+
raise Grape::Exceptions::ValidationErrors.new(exceptions: validation_exceptions, headers: header) if validation_exceptions
|
|
323
223
|
end
|
|
324
224
|
|
|
325
225
|
def run_filters(filters, type = :other)
|
|
326
|
-
|
|
327
|
-
|
|
226
|
+
return if filters.blank?
|
|
227
|
+
|
|
228
|
+
instrument_run_filters(filters, type) do
|
|
229
|
+
filters.each { |filter| instance_eval(&filter) }
|
|
328
230
|
end
|
|
329
|
-
post_extension = DSL::InsideRoute.post_filter_methods(type)
|
|
330
|
-
extend post_extension if post_extension
|
|
331
231
|
end
|
|
332
232
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
233
|
+
attr_reader :befores, :before_validations, :after_validations, :afters, :finallies, :config
|
|
234
|
+
|
|
235
|
+
def options?
|
|
236
|
+
options_route_enabled && env[Rack::REQUEST_METHOD] == Rack::OPTIONS
|
|
337
237
|
end
|
|
338
238
|
|
|
339
|
-
|
|
340
|
-
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
attr_reader :before_filter_passed
|
|
242
|
+
|
|
243
|
+
# Instrument helpers. Each guards on +listening?+ so that with no subscriber
|
|
244
|
+
# the payload Hash and notification machinery are skipped and the block runs
|
|
245
|
+
# directly (no added allocations); the block is forwarded anonymously so
|
|
246
|
+
# nothing is allocated unless a subscriber is present.
|
|
247
|
+
def instrument_run(&)
|
|
248
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run.grape')
|
|
249
|
+
|
|
250
|
+
ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env:, &)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def instrument_render(&)
|
|
254
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_render.grape')
|
|
255
|
+
|
|
256
|
+
ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: self, &)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def instrument_run_validators(validators, request, &)
|
|
260
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run_validators.grape')
|
|
261
|
+
|
|
262
|
+
ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators:, request:, &)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def instrument_run_filters(filters, type, &)
|
|
266
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run_filters.grape')
|
|
267
|
+
|
|
268
|
+
ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters:, type:, &)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def compile!
|
|
272
|
+
@app = config.app || build_stack
|
|
273
|
+
warn_unauthenticated_mounted_app
|
|
274
|
+
@helpers = build_helpers
|
|
275
|
+
callbacks = inheritable_setting.callbacks
|
|
276
|
+
@befores = callbacks.fetch(:before)
|
|
277
|
+
@before_validations = callbacks.fetch(:before_validation)
|
|
278
|
+
@after_validations = callbacks.fetch(:after_validation)
|
|
279
|
+
@afters = callbacks.fetch(:after)
|
|
280
|
+
@finallies = callbacks.fetch(:finally)
|
|
281
|
+
@build_params_with = inheritable_setting.build_params_with
|
|
282
|
+
@prototype = build_prototype
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# The object each request is copied from.
|
|
286
|
+
#
|
|
287
|
+
# Helpers reach an endpoint by module inclusion, and +Object#dup+ does not
|
|
288
|
+
# carry a singleton class over — so including them on the per-request copy
|
|
289
|
+
# meant giving every request a brand-new singleton class, whose method
|
|
290
|
+
# cache then started cold for the helpers, the route block and every DSL
|
|
291
|
+
# method the endpoint answers. Include them once into a subclass owned by
|
|
292
|
+
# this endpoint instead, and copy this endpoint's state onto an instance of
|
|
293
|
+
# it: the per-request copy already answers to the helpers, against a class
|
|
294
|
+
# that has been warm since boot. It also keeps request handling free of
|
|
295
|
+
# class mutation.
|
|
296
|
+
#
|
|
297
|
+
# Endpoints with no helpers are their own prototype, exactly as before.
|
|
298
|
+
#
|
|
299
|
+
# +allocate+ rather than +new+ because the constructor rebuilds state from
|
|
300
|
+
# the DSL's arguments — a fresh settings copy, a fresh +@config+, another
|
|
301
|
+
# +block_to_unbound_method+ — and would still not hold what +compile!+ has
|
|
302
|
+
# just computed. What is wanted is what +dup+ itself does (allocate, then
|
|
303
|
+
# copy the ivars over) across a class boundary, which neither +dup+ nor
|
|
304
|
+
# +initialize_copy+ will do: both insist on the receiver's own class. So
|
|
305
|
+
# the copy is written out here. Taking all of them is safe because
|
|
306
|
+
# everything set past this point (+routes+, +namespace+) is route-building
|
|
307
|
+
# memoization the request path never reads.
|
|
308
|
+
def build_prototype
|
|
309
|
+
return self unless @helpers
|
|
310
|
+
|
|
311
|
+
klass = Class.new(self.class)
|
|
312
|
+
# Anonymous classes answer nil to +name+ and render as a bare address,
|
|
313
|
+
# which would reach anything that reports on the endpoint — +inspect+
|
|
314
|
+
# here, a logger or an error tracker elsewhere. The annotation is what
|
|
315
|
+
# +set_temporary_name+ is for; a plain constant path is rejected, which
|
|
316
|
+
# is right, since this class is not reachable under that name.
|
|
317
|
+
klass.set_temporary_name("#{self.class}(helpers)")
|
|
318
|
+
klass.include(@helpers)
|
|
319
|
+
prototype = klass.allocate
|
|
320
|
+
# Everything but the prototype slot itself, so that recompiling (a
|
|
321
|
+
# remount) does not leave the new prototype holding the one it replaces.
|
|
322
|
+
(instance_variables - [:@prototype]).each { |name| prototype.instance_variable_set(name, instance_variable_get(name)) }
|
|
323
|
+
prototype
|
|
324
|
+
end
|
|
341
325
|
|
|
342
|
-
|
|
343
|
-
|
|
326
|
+
def to_routes
|
|
327
|
+
route_options = config.route_options
|
|
328
|
+
params = config.params
|
|
329
|
+
path_settings = inheritable_setting.path_settings
|
|
330
|
+
forward_match = bare_rack_app?
|
|
331
|
+
version = prepare_version(inheritable_setting.version)
|
|
332
|
+
prefix = inheritable_setting.root_prefix
|
|
333
|
+
requirements = prepare_routes_requirements(config.requirements)
|
|
334
|
+
anchor = config.anchor
|
|
335
|
+
settings = inheritable_setting.route_settings
|
|
336
|
+
|
|
337
|
+
config.http_methods.flat_map do |method|
|
|
338
|
+
config.path.map do |path|
|
|
339
|
+
pattern = Grape::Router::Pattern.build(
|
|
340
|
+
path:,
|
|
341
|
+
namespace:,
|
|
342
|
+
settings: path_settings,
|
|
343
|
+
anchor:,
|
|
344
|
+
params:,
|
|
345
|
+
version:,
|
|
346
|
+
requirements:
|
|
347
|
+
)
|
|
348
|
+
Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, params:, namespace:, prefix:, settings:)
|
|
349
|
+
end
|
|
344
350
|
end
|
|
345
351
|
end
|
|
346
352
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
353
|
+
# True when a bare Rack app (anything that isn't a Grape app) is mounted at
|
|
354
|
+
# this endpoint. Such an app is called directly and matched by path prefix
|
|
355
|
+
# rather than an anchored route.
|
|
356
|
+
def bare_rack_app?
|
|
357
|
+
config.app && !config.app.is_a?(Grape::Mountable)
|
|
350
358
|
end
|
|
351
359
|
|
|
352
|
-
|
|
360
|
+
def prepare_routes_requirements(route_options_requirements)
|
|
361
|
+
namespace_requirements = inheritable_setting.namespace_requirements
|
|
362
|
+
namespace_requirements << route_options_requirements if route_options_requirements.present?
|
|
363
|
+
namespace_requirements.reduce({}, :merge)
|
|
364
|
+
end
|
|
353
365
|
|
|
354
|
-
def
|
|
366
|
+
def prepare_version(namespace_inheritable_version)
|
|
367
|
+
return if namespace_inheritable_version.blank?
|
|
368
|
+
|
|
369
|
+
namespace_inheritable_version.length == 1 ? namespace_inheritable_version.first : namespace_inheritable_version
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def build_stack
|
|
355
373
|
stack = Grape::Middleware::Stack.new
|
|
356
374
|
|
|
357
|
-
content_types =
|
|
358
|
-
format =
|
|
375
|
+
content_types = inheritable_setting.content_types
|
|
376
|
+
format = inheritable_setting.format
|
|
359
377
|
|
|
360
378
|
stack.use Rack::Head
|
|
361
379
|
stack.use Rack::Lint if lint?
|
|
362
|
-
stack.use
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers),
|
|
374
|
-
all_rescue_handler: namespace_inheritable(:all_rescue_handler),
|
|
375
|
-
grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler)
|
|
376
|
-
|
|
377
|
-
stack.concat namespace_stackable(:middleware)
|
|
378
|
-
|
|
379
|
-
if namespace_inheritable(:version).present?
|
|
380
|
-
stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
|
|
381
|
-
versions: namespace_inheritable(:version).flatten,
|
|
382
|
-
version_options: namespace_inheritable(:version_options),
|
|
383
|
-
prefix: namespace_inheritable(:root_prefix),
|
|
384
|
-
mount_path: namespace_stackable(:mount_path).first
|
|
380
|
+
stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types)
|
|
381
|
+
|
|
382
|
+
stack.concat inheritable_setting.middleware
|
|
383
|
+
|
|
384
|
+
if inheritable_setting.version.present?
|
|
385
|
+
version_options = inheritable_setting.version_options
|
|
386
|
+
stack.use Grape::Middleware::Versioner.using(version_options.using),
|
|
387
|
+
versions: inheritable_setting.version.flatten,
|
|
388
|
+
version_options:,
|
|
389
|
+
prefix: inheritable_setting.root_prefix,
|
|
390
|
+
mount_path: inheritable_setting.mount_path
|
|
385
391
|
end
|
|
386
392
|
|
|
387
393
|
stack.use Grape::Middleware::Formatter,
|
|
388
|
-
format
|
|
389
|
-
default_format:
|
|
390
|
-
content_types
|
|
391
|
-
formatters:
|
|
392
|
-
parsers:
|
|
394
|
+
format:,
|
|
395
|
+
default_format: inheritable_setting.default_format || :txt,
|
|
396
|
+
content_types:,
|
|
397
|
+
formatters: inheritable_setting.formatters,
|
|
398
|
+
parsers: inheritable_setting.parsers
|
|
393
399
|
|
|
394
400
|
builder = stack.build
|
|
395
401
|
builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
|
|
396
402
|
builder.to_app
|
|
397
403
|
end
|
|
398
404
|
|
|
405
|
+
def error_middleware_options(format, content_types)
|
|
406
|
+
setting = inheritable_setting
|
|
407
|
+
{
|
|
408
|
+
format:,
|
|
409
|
+
content_types:,
|
|
410
|
+
default_status: setting.default_error_status,
|
|
411
|
+
rescue_all: setting.rescue_all?,
|
|
412
|
+
rescue_grape_exceptions: setting.rescue_grape_exceptions?,
|
|
413
|
+
default_error_formatter: setting.default_error_formatter,
|
|
414
|
+
error_formatters: setting.error_formatters,
|
|
415
|
+
rescue_options: setting.rescue_options,
|
|
416
|
+
rescue_handlers: setting.rescue_handlers,
|
|
417
|
+
base_only_rescue_handlers: setting.base_only_rescue_handlers,
|
|
418
|
+
all_rescue_handler: setting.all_rescue_handler,
|
|
419
|
+
grape_exceptions_rescue_handler: setting.grape_exceptions_rescue_handler,
|
|
420
|
+
internal_grape_exceptions_rescue_handler: setting.internal_grape_exceptions_rescue_handler
|
|
421
|
+
}
|
|
422
|
+
end
|
|
423
|
+
|
|
399
424
|
def build_helpers
|
|
400
|
-
helpers =
|
|
425
|
+
helpers = inheritable_setting.helpers
|
|
401
426
|
return if helpers.empty?
|
|
402
427
|
|
|
403
428
|
Module.new { helpers.each { |mod_to_include| include mod_to_include } }
|
|
404
429
|
end
|
|
405
430
|
|
|
431
|
+
# A bare Rack app mounted with +mount+ is called directly (see +compile!+):
|
|
432
|
+
# it does not go through +build_stack+, so the API's authentication
|
|
433
|
+
# middleware never runs and the mount is reachable unauthenticated. Mounted
|
|
434
|
+
# Grape APIs are unaffected because they rebuild their own stack from the
|
|
435
|
+
# inherited settings. Warn so this bypass isn't silent.
|
|
436
|
+
def warn_unauthenticated_mounted_app
|
|
437
|
+
return unless bare_rack_app?
|
|
438
|
+
return unless inheritable_setting.auth
|
|
439
|
+
|
|
440
|
+
warn "Grape: #{config.app} is mounted under an API that declares authentication, but authentication " \
|
|
441
|
+
'middleware does not wrap mounted Rack applications. Requests to this mount are not authenticated by Grape.'
|
|
442
|
+
end
|
|
443
|
+
|
|
406
444
|
def build_response_cookies
|
|
445
|
+
return unless request.cookies?
|
|
446
|
+
|
|
407
447
|
response_cookies do |name, value|
|
|
408
|
-
cookie_value = value.is_a?(Hash) ? value : { value:
|
|
448
|
+
cookie_value = value.is_a?(Hash) ? value : { value: }
|
|
409
449
|
Rack::Utils.set_cookie_header! header, name, cookie_value
|
|
410
450
|
end
|
|
411
451
|
end
|
|
412
452
|
|
|
413
453
|
def lint?
|
|
414
|
-
|
|
454
|
+
inheritable_setting.lint? || Grape.config.lint
|
|
415
455
|
end
|
|
416
456
|
end
|
|
417
457
|
end
|