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/env.rb
CHANGED
|
@@ -11,10 +11,15 @@ module Grape
|
|
|
11
11
|
API_VENDOR = 'api.vendor'
|
|
12
12
|
API_FORMAT = 'api.format'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
GRAPE_REQUEST_HEADERS = 'grape.request.headers'
|
|
16
|
-
GRAPE_REQUEST_PARAMS = 'grape.request.params'
|
|
14
|
+
GRAPE_NORMALIZED_PATH = 'grape.normalized_path'
|
|
17
15
|
GRAPE_ROUTING_ARGS = 'grape.routing_args'
|
|
18
16
|
GRAPE_ALLOWED_METHODS = 'grape.allowed_methods'
|
|
17
|
+
GRAPE_EXCEPTION = 'grape.exception'
|
|
18
|
+
|
|
19
|
+
# Not a Grape-owned key: the de-facto convention for an exception that was
|
|
20
|
+
# handled rather than raised, which is how error trackers find one they
|
|
21
|
+
# never saw propagate. sentry-ruby, for one, collects
|
|
22
|
+
# +env['rack.exception'] || env['sinatra.error']+.
|
|
23
|
+
RACK_EXCEPTION = 'rack.exception'
|
|
19
24
|
end
|
|
20
25
|
end
|
|
@@ -4,34 +4,40 @@ module Grape
|
|
|
4
4
|
module ErrorFormatter
|
|
5
5
|
class Base
|
|
6
6
|
class << self
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
# Custom error formatters override +call+. The +error+ is a frozen
|
|
8
|
+
# {Grape::Exceptions::ErrorResponse} carrying +status+/+message+/
|
|
9
|
+
# +headers+/+backtrace+/+original_exception+. +env+ is the Rack env
|
|
10
|
+
# (needed by entity-presenter resolution). +include_backtrace+ and
|
|
11
|
+
# +include_original_exception+ are the request-time toggles set by
|
|
12
|
+
# +rescue_from+; the base implementation embeds the corresponding
|
|
13
|
+
# fields in the response body when they are true.
|
|
14
|
+
def call(error:, env: nil, include_backtrace: false, include_original_exception: false)
|
|
15
|
+
wrapped_message = wrap_message(present(error.message, env))
|
|
12
16
|
if wrapped_message.is_a?(Hash)
|
|
13
|
-
wrapped_message[:backtrace] = backtrace if
|
|
14
|
-
wrapped_message[:original_exception] = original_exception.inspect if
|
|
17
|
+
wrapped_message[:backtrace] = error.backtrace if include_backtrace && error.backtrace.present?
|
|
18
|
+
wrapped_message[:original_exception] = error.original_exception.inspect if include_original_exception && error.original_exception
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
format_structured_message(wrapped_message)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def present(message, env)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# error! accepts a message hash with an optional :with key specifying the entity presenter.
|
|
26
|
+
# Extract it here so the presenter can be resolved and the key is not serialized in the response.
|
|
27
|
+
# See spec/integration/grape_entity/entity_spec.rb for examples.
|
|
28
|
+
with = nil
|
|
29
|
+
payload = message
|
|
30
|
+
if message.is_a?(Hash) && message.key?(:with)
|
|
31
|
+
payload = message.dup
|
|
32
|
+
with = payload.delete(:with)
|
|
26
33
|
end
|
|
27
34
|
|
|
28
|
-
presenter = env[Grape::Env::API_ENDPOINT].entity_class_for_obj(
|
|
35
|
+
presenter = with || env[Grape::Env::API_ENDPOINT].entity_class_for_obj(payload)
|
|
29
36
|
|
|
30
37
|
unless presenter || env[Grape::Env::GRAPE_ROUTING_ARGS].nil?
|
|
31
38
|
# env['api.endpoint'].route does not work when the error occurs within a middleware
|
|
32
39
|
# the Endpoint does not have a valid env at this moment
|
|
33
40
|
http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
|
|
34
|
-
|
|
35
41
|
found_code = http_codes.find do |http_code|
|
|
36
42
|
(http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
|
|
37
43
|
end if env[Grape::Env::API_ENDPOINT].request
|
|
@@ -39,25 +45,25 @@ module Grape
|
|
|
39
45
|
presenter = found_code[2] if found_code
|
|
40
46
|
end
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
embeds = { env: env }
|
|
44
|
-
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
|
|
45
|
-
presented_message = presenter.represent(presented_message, embeds).serializable_hash
|
|
46
|
-
end
|
|
48
|
+
return payload unless presenter
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
embeds = { env: }
|
|
51
|
+
embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
|
|
52
|
+
presenter.represent(payload, embeds).serializable_hash
|
|
49
53
|
end
|
|
50
54
|
|
|
51
55
|
def wrap_message(message)
|
|
52
56
|
return message if message.is_a?(Hash)
|
|
53
57
|
|
|
54
|
-
{ message:
|
|
58
|
+
{ message: }
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
def format_structured_message(_structured_message)
|
|
58
62
|
raise NotImplementedError
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
private
|
|
66
|
+
|
|
61
67
|
def inherited(klass)
|
|
62
68
|
super
|
|
63
69
|
ErrorFormatter.register(klass)
|
|
@@ -11,6 +11,12 @@ module Grape
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def wrap_message(message)
|
|
14
|
+
# Use +is_a?+ rather than +case/when+ here. +case/when Hash+ matches via
|
|
15
|
+
# +Module#===+, a C-level real-class check that ignores delegation, so a
|
|
16
|
+
# +SimpleDelegator+ wrapping a Hash (e.g. the +OutputBuilder+ returned by
|
|
17
|
+
# +Grape::Entity#serializable_hash+ when an error is presented via an entity)
|
|
18
|
+
# would fall through and be wrapped in a spurious +{ error: ... }+ envelope.
|
|
19
|
+
# +is_a?+ is forwarded by the delegator to the wrapped Hash, so it matches.
|
|
14
20
|
return message if message.is_a?(Hash)
|
|
15
21
|
return message.as_json if message.is_a?(Exceptions::ValidationErrors)
|
|
16
22
|
|
|
@@ -5,16 +5,16 @@ module Grape
|
|
|
5
5
|
class Txt < Base
|
|
6
6
|
def self.format_structured_message(structured_message)
|
|
7
7
|
message = structured_message[:message] || Grape::Json.dump(structured_message)
|
|
8
|
-
Array.wrap(message)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
final_message = Array.wrap(message)
|
|
9
|
+
if structured_message.key?(:backtrace)
|
|
10
|
+
final_message << 'backtrace:'
|
|
11
|
+
final_message.concat(structured_message[:backtrace])
|
|
12
|
+
end
|
|
13
|
+
if structured_message.key?(:original_exception)
|
|
14
|
+
final_message << 'original exception:'
|
|
15
|
+
final_message << structured_message[:original_exception]
|
|
16
|
+
end
|
|
17
|
+
final_message.join("\r\n ")
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -6,10 +6,14 @@ module Grape
|
|
|
6
6
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# Answers nil when nothing is registered for the format, the way
|
|
10
|
+
# +Parser.parser_for+ does. What to fall back to then is the API's own
|
|
11
|
+
# +default_error_formatter+, which is the caller's state rather than the
|
|
12
|
+
# registry's: +Middleware::Error+ holds it and applies it.
|
|
13
|
+
def formatter_for(format, error_formatters = nil)
|
|
10
14
|
return error_formatters[format] if error_formatters&.key?(format)
|
|
11
15
|
|
|
12
|
-
registry[format]
|
|
16
|
+
registry[format]
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Exceptions
|
|
5
5
|
class Base < StandardError
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
include Grape::Util::Translation
|
|
7
|
+
|
|
8
|
+
MESSAGE_STEPS = %w[problem summary resolution].to_h { |s| [s.to_sym, s.capitalize] }.freeze
|
|
9
9
|
|
|
10
10
|
attr_reader :status, :headers
|
|
11
11
|
|
|
@@ -17,58 +17,34 @@ module Grape
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def [](index)
|
|
20
|
-
|
|
20
|
+
__send__ index
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
private
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# if BASE_ATTRIBUTES_KEY.key respond to a Hash, means it may have problem , summary and resolution
|
|
28
|
-
def compose_message(key, **attributes)
|
|
29
|
-
short_message = translate_message(key, attributes)
|
|
25
|
+
def compose_message(key, **)
|
|
26
|
+
short_message = translate_message(key, **)
|
|
30
27
|
return short_message unless short_message.is_a?(Hash)
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def each_steps(key, attributes)
|
|
38
|
-
return enum_for(:each_steps, key, attributes) unless block_given?
|
|
39
|
-
|
|
40
|
-
yield 'Problem', translate_message(:"#{key}.problem", attributes)
|
|
41
|
-
yield 'Summary', translate_message(:"#{key}.summary", attributes)
|
|
42
|
-
yield 'Resolution', translate_message(:"#{key}.resolution", attributes)
|
|
43
|
-
end
|
|
29
|
+
MESSAGE_STEPS.filter_map do |step, label|
|
|
30
|
+
next unless short_message.key?(step)
|
|
44
31
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end.join(', ')
|
|
32
|
+
detail = translate_message(:"#{key}.#{step}", **)
|
|
33
|
+
"\n#{label}:\n #{detail}" if detail.present?
|
|
34
|
+
end.join
|
|
49
35
|
end
|
|
50
36
|
|
|
51
|
-
def translate_message(
|
|
52
|
-
case
|
|
37
|
+
def translate_message(translation_key, **)
|
|
38
|
+
case translation_key
|
|
53
39
|
when Symbol
|
|
54
|
-
translate(
|
|
40
|
+
translate(translation_key, **)
|
|
41
|
+
when Hash
|
|
42
|
+
translation_key => { key:, **opts }
|
|
43
|
+
translate(key, **opts)
|
|
55
44
|
when Proc
|
|
56
|
-
|
|
57
|
-
else
|
|
58
|
-
key
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def translate(key, options)
|
|
63
|
-
message = ::I18n.translate(key, **options)
|
|
64
|
-
message.presence || fallback_message(key, options)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def fallback_message(key, options)
|
|
68
|
-
if ::I18n.enforce_available_locales && ::I18n.available_locales.exclude?(FALLBACK_LOCALE)
|
|
69
|
-
key
|
|
45
|
+
translation_key.call
|
|
70
46
|
else
|
|
71
|
-
|
|
47
|
+
translation_key
|
|
72
48
|
end
|
|
73
49
|
end
|
|
74
50
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Exceptions
|
|
5
|
+
# Value object representing the payload thrown via `throw :error, ...`
|
|
6
|
+
# and consumed by `Middleware::Error#error_response`. Replaces the
|
|
7
|
+
# implicit-schema Hash that previously circulated between throw sites
|
|
8
|
+
# and the error middleware.
|
|
9
|
+
ErrorResponse = Data.define(:status, :message, :headers, :backtrace, :original_exception) do
|
|
10
|
+
def initialize(status: nil, message: nil, headers: nil, backtrace: nil, original_exception: nil)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
"#<#{self.class.name} status=#{status.inspect} message=#{message.inspect} headers=#{headers.inspect}>"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The backtrace is deliberately left unset: +Exception#backtrace+ builds
|
|
19
|
+
# the whole Array of location strings, and the response only renders one
|
|
20
|
+
# when the API asked for it. The exception travels along, so
|
|
21
|
+
# +Middleware::Error#error_response+ can still materialize it there.
|
|
22
|
+
def self.from_exception(exception)
|
|
23
|
+
new(
|
|
24
|
+
status: exception.status,
|
|
25
|
+
message: exception.message,
|
|
26
|
+
headers: exception.headers,
|
|
27
|
+
original_exception: exception
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Normalize heterogeneous inputs into an ErrorResponse. Preserves the
|
|
32
|
+
# public contract that users can still `throw :error, hash` from their
|
|
33
|
+
# own middleware or `rescue_from` handlers.
|
|
34
|
+
def self.coerce(input)
|
|
35
|
+
case input
|
|
36
|
+
when ErrorResponse
|
|
37
|
+
input
|
|
38
|
+
when Grape::Exceptions::Base
|
|
39
|
+
from_exception(input)
|
|
40
|
+
when Hash
|
|
41
|
+
new(**input.slice(:status, :message, :headers, :backtrace, :original_exception))
|
|
42
|
+
else
|
|
43
|
+
new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class IncompatibleOptionValues < Base
|
|
6
6
|
def initialize(option1, value1, option2, value2)
|
|
7
|
-
super(message: compose_message(:incompatible_option_values, option1
|
|
7
|
+
super(message: compose_message(:incompatible_option_values, option1:, value1:, option2:, value2:))
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Exceptions
|
|
5
|
+
# Raised internally when a +rescue_from+ handler itself raises an
|
|
6
|
+
# unrecognised exception. The framework substitutes the original
|
|
7
|
+
# exception with this safe stand-in for rendering, while preserving
|
|
8
|
+
# the original on +env[Grape::Env::GRAPE_EXCEPTION]+ for upstream
|
|
9
|
+
# observability (loggers, error trackers, etc.).
|
|
10
|
+
class InternalServerError < Base
|
|
11
|
+
def initialize
|
|
12
|
+
super(status: 500, message: compose_message(:internal_server_error))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class InvalidAcceptHeader < Base
|
|
6
6
|
def initialize(message, headers)
|
|
7
|
-
super(message: compose_message(:invalid_accept_header, message:
|
|
7
|
+
super(message: compose_message(:invalid_accept_header, message:), status: 406, headers:)
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class InvalidFormatter < Base
|
|
6
6
|
def initialize(klass, to_format)
|
|
7
|
-
super(message: compose_message(:invalid_formatter, klass
|
|
7
|
+
super(message: compose_message(:invalid_formatter, klass:, to_format:))
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class InvalidMessageBody < Base
|
|
6
6
|
def initialize(body_format)
|
|
7
|
-
super(message: compose_message(:invalid_message_body, body_format:
|
|
7
|
+
super(message: compose_message(:invalid_message_body, body_format:), status: 400)
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class InvalidVersionHeader < Base
|
|
6
6
|
def initialize(message, headers)
|
|
7
|
-
super(message: compose_message(:invalid_version_header, message:
|
|
7
|
+
super(message: compose_message(:invalid_version_header, message:), status: 406, headers:)
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class InvalidVersionerOption < Base
|
|
6
6
|
def initialize(strategy)
|
|
7
|
-
super(message: compose_message(:invalid_versioner_option, strategy:
|
|
7
|
+
super(message: compose_message(:invalid_versioner_option, strategy:))
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class UnknownParamsBuilder < Base
|
|
6
6
|
def initialize(params_builder_type)
|
|
7
|
-
super(message: compose_message(:unknown_params_builder, params_builder_type:
|
|
7
|
+
super(message: compose_message(:unknown_params_builder, params_builder_type:))
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
class UnknownValidator < Base
|
|
6
6
|
def initialize(validator_type)
|
|
7
|
-
super(message: compose_message(:unknown_validator, validator_type:
|
|
7
|
+
super(message: compose_message(:unknown_validator, validator_type:))
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -3,16 +3,26 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Exceptions
|
|
5
5
|
class Validation < Base
|
|
6
|
-
|
|
6
|
+
EMPTY_BACKTRACE = [].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :params, :message_key
|
|
7
9
|
|
|
8
10
|
def initialize(params:, message: nil, status: nil, headers: nil)
|
|
9
|
-
@params = params
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
@params = Array(params)
|
|
12
|
+
translated =
|
|
13
|
+
if message
|
|
14
|
+
@message_key = case message
|
|
15
|
+
when Symbol then message
|
|
16
|
+
when Hash then message[:key]
|
|
17
|
+
end
|
|
18
|
+
translate_message(message)
|
|
19
|
+
end
|
|
14
20
|
|
|
15
|
-
super(status
|
|
21
|
+
super(status:, message: translated, headers:)
|
|
22
|
+
# Pre-seed the backtrace so Ruby's raise skips capture. Validation errors are
|
|
23
|
+
# a hot path (raised per bad attribute) and end up as 400 Bad Request responses;
|
|
24
|
+
# backtraces here point into Grape internals and have no diagnostic value.
|
|
25
|
+
set_backtrace(EMPTY_BACKTRACE)
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
# Remove all the unnecessary stuff from Grape::Exceptions::Base like status
|
|
@@ -20,6 +30,14 @@ module Grape
|
|
|
20
30
|
def as_json(*_args)
|
|
21
31
|
to_s
|
|
22
32
|
end
|
|
33
|
+
|
|
34
|
+
# Returns +self+ so callers (e.g. +ValidationErrors#initialize+) can treat
|
|
35
|
+
# a single +Validation+ and a +ValidationArrayErrors+ wrapper uniformly via
|
|
36
|
+
# +flat_map(&:errors)+ — Array returns flatten in, non-Array returns
|
|
37
|
+
# (i.e. this +self+) append as one element.
|
|
38
|
+
def errors
|
|
39
|
+
self
|
|
40
|
+
end
|
|
23
41
|
end
|
|
24
42
|
end
|
|
25
43
|
end
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Exceptions
|
|
5
5
|
class ValidationArrayErrors < Base
|
|
6
|
+
EMPTY_BACKTRACE = [].freeze
|
|
7
|
+
|
|
6
8
|
attr_reader :errors
|
|
7
9
|
|
|
8
10
|
def initialize(errors)
|
|
9
11
|
super()
|
|
10
12
|
@errors = errors
|
|
13
|
+
# Skip backtrace capture — see Grape::Exceptions::Validation for rationale.
|
|
14
|
+
set_backtrace(EMPTY_BACKTRACE)
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -3,24 +3,11 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Exceptions
|
|
5
5
|
class ValidationErrors < Base
|
|
6
|
-
ERRORS_FORMAT_KEY = 'grape.errors.format'
|
|
7
|
-
DEFAULT_ERRORS_FORMAT = '%<attributes>s %<message>s'
|
|
8
|
-
|
|
9
|
-
include Enumerable
|
|
10
|
-
|
|
11
6
|
attr_reader :errors
|
|
12
7
|
|
|
13
|
-
def initialize(
|
|
14
|
-
@errors = errors.group_by(&:params)
|
|
15
|
-
super(message: full_messages.join(', '), status: 400, headers:
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def each
|
|
19
|
-
errors.each_pair do |attribute, errors|
|
|
20
|
-
errors.each do |error|
|
|
21
|
-
yield attribute, error
|
|
22
|
-
end
|
|
23
|
-
end
|
|
8
|
+
def initialize(exceptions: [], headers: {})
|
|
9
|
+
@errors = exceptions.flat_map(&:errors).group_by(&:params)
|
|
10
|
+
super(message: full_messages.join(', '), status: 400, headers:)
|
|
24
11
|
end
|
|
25
12
|
|
|
26
13
|
def as_json(**_opts)
|
|
@@ -37,17 +24,28 @@ module Grape
|
|
|
37
24
|
end
|
|
38
25
|
|
|
39
26
|
def full_messages
|
|
40
|
-
messages =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
messages = errors.flat_map do |attributes, errs|
|
|
28
|
+
errs.map do |error|
|
|
29
|
+
translate(
|
|
30
|
+
:format,
|
|
31
|
+
scope: 'grape.errors',
|
|
32
|
+
default: '%<attributes>s %<message>s',
|
|
33
|
+
attributes: translate_attributes(attributes),
|
|
34
|
+
message: error.message
|
|
35
|
+
)
|
|
36
|
+
end
|
|
47
37
|
end
|
|
48
38
|
messages.uniq!
|
|
49
39
|
messages
|
|
50
40
|
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def translate_attributes(keys)
|
|
45
|
+
keys.map do |key|
|
|
46
|
+
translate(key, scope: 'grape.errors.attributes', default: key.to_s)
|
|
47
|
+
end.join(', ')
|
|
48
|
+
end
|
|
51
49
|
end
|
|
52
50
|
end
|
|
53
51
|
end
|
data/lib/grape/formatter/json.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Grape
|
|
|
5
5
|
class SerializableHash < Base
|
|
6
6
|
class << self
|
|
7
7
|
def call(object, _env)
|
|
8
|
+
return object.to_s if object.is_a?(Grape::PrecompiledJson)
|
|
8
9
|
return object if object.is_a?(String)
|
|
9
10
|
return ::Grape::Json.dump(serialize(object)) if serializable?(object)
|
|
10
11
|
return object.to_json if object.respond_to?(:to_json)
|
|
@@ -19,15 +20,11 @@ module Grape
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def serialize(object)
|
|
22
|
-
if object.respond_to?
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
object.transform_values { |v| serialize(v) }
|
|
28
|
-
else
|
|
29
|
-
object
|
|
30
|
-
end
|
|
23
|
+
return object.serializable_hash if object.respond_to?(:serializable_hash)
|
|
24
|
+
return object.map(&:serializable_hash) if array_serializable?(object)
|
|
25
|
+
return object.transform_values { |v| serialize(v) } if object.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
object
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
def array_serializable?(object)
|