grape 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. data/lib/grape/middleware/globals.rb +0 -14
@@ -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).tap do |final_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
- end.join("\r\n ")
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
@@ -5,7 +5,7 @@ module Grape
5
5
  class Base < StandardError
6
6
  include Grape::Util::Translation
7
7
 
8
- MESSAGE_STEPS = %w[problem summary resolution].to_h { |s| [s, s.capitalize] }.freeze
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
 
@@ -27,6 +27,8 @@ module Grape
27
27
  return short_message unless short_message.is_a?(Hash)
28
28
 
29
29
  MESSAGE_STEPS.filter_map do |step, label|
30
+ next unless short_message.key?(step)
31
+
30
32
  detail = translate_message(:"#{key}.#{step}", **)
31
33
  "\n#{label}:\n #{detail}" if detail.present?
32
34
  end.join
@@ -0,0 +1,45 @@
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
+ def self.from_exception(exception)
19
+ new(
20
+ status: exception.status,
21
+ message: exception.message,
22
+ headers: exception.headers,
23
+ backtrace: exception.backtrace,
24
+ original_exception: exception
25
+ )
26
+ end
27
+
28
+ # Normalize heterogeneous inputs into an ErrorResponse. Preserves the
29
+ # public contract that users can still `throw :error, hash` from their
30
+ # own middleware or `rescue_from` handlers.
31
+ def self.coerce(input)
32
+ case input
33
+ when ErrorResponse
34
+ input
35
+ when Grape::Exceptions::Base
36
+ from_exception(input)
37
+ when Hash
38
+ new(**input.slice(:status, :message, :headers, :backtrace, :original_exception))
39
+ else
40
+ new
41
+ end
42
+ end
43
+ end
44
+ end
45
+ 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
@@ -3,6 +3,8 @@
3
3
  module Grape
4
4
  module Exceptions
5
5
  class Validation < Base
6
+ EMPTY_BACKTRACE = [].freeze
7
+
6
8
  attr_reader :params, :message_key
7
9
 
8
10
  def initialize(params:, message: nil, status: nil, headers: nil)
@@ -16,6 +18,10 @@ module Grape
16
18
  end
17
19
 
18
20
  super(status:, message:, headers:)
21
+ # Pre-seed the backtrace so Ruby's raise skips capture. Validation errors are
22
+ # a hot path (raised per bad attribute) and end up as 400 Bad Request responses;
23
+ # backtraces here point into Grape internals and have no diagnostic value.
24
+ set_backtrace(EMPTY_BACKTRACE)
19
25
  end
20
26
 
21
27
  # Remove all the unnecessary stuff from Grape::Exceptions::Base like status
@@ -23,6 +29,14 @@ module Grape
23
29
  def as_json(*_args)
24
30
  to_s
25
31
  end
32
+
33
+ # Returns +self+ so callers (e.g. +ValidationErrors#initialize+) can treat
34
+ # a single +Validation+ and a +ValidationArrayErrors+ wrapper uniformly via
35
+ # +flat_map(&:errors)+ — Array returns flatten in, non-Array returns
36
+ # (i.e. this +self+) append as one element.
37
+ def errors
38
+ self
39
+ end
26
40
  end
27
41
  end
28
42
  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,23 +3,13 @@
3
3
  module Grape
4
4
  module Exceptions
5
5
  class ValidationErrors < Base
6
- include Enumerable
7
-
8
6
  attr_reader :errors
9
7
 
10
- def initialize(errors: [], headers: {})
11
- @errors = errors.group_by(&:params)
8
+ def initialize(exceptions: [], headers: {})
9
+ @errors = exceptions.flat_map(&:errors).group_by(&:params)
12
10
  super(message: full_messages.join(', '), status: 400, headers:)
13
11
  end
14
12
 
15
- def each
16
- errors.each_pair do |attribute, errors|
17
- errors.each do |error|
18
- yield attribute, error
19
- end
20
- end
21
- end
22
-
23
13
  def as_json(**_opts)
24
14
  errors.map do |k, v|
25
15
  {
@@ -34,14 +24,16 @@ module Grape
34
24
  end
35
25
 
36
26
  def full_messages
37
- messages = map do |attributes, error|
38
- translate(
39
- :format,
40
- scope: 'grape.errors',
41
- default: '%<attributes>s %<message>s',
42
- attributes: translate_attributes(attributes),
43
- message: error.message
44
- )
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
45
37
  end
46
38
  messages.uniq!
47
39
  messages
@@ -19,15 +19,11 @@ module Grape
19
19
  end
20
20
 
21
21
  def serialize(object)
22
- if object.respond_to? :serializable_hash
23
- object.serializable_hash
24
- elsif array_serializable?(object)
25
- object.map(&:serializable_hash)
26
- elsif object.is_a?(Hash)
27
- object.transform_values { |v| serialize(v) }
28
- else
29
- object
30
- end
22
+ return object.serializable_hash if object.respond_to?(:serializable_hash)
23
+ return object.map(&:serializable_hash) if array_serializable?(object)
24
+ return object.transform_values { |v| serialize(v) } if object.is_a?(Hash)
25
+
26
+ object
31
27
  end
32
28
 
33
29
  def array_serializable?(object)
data/lib/grape/json.rb CHANGED
@@ -1,8 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- if defined?(::MultiJson)
5
- Json = ::MultiJson
4
+ if defined?(::MultiJSON)
5
+ # Since multi_json 1.21.0, MultiJSON.dump is deprecated in favor of
6
+ # MultiJSON.generate (removed in 2.0). Keep Grape's dump surface but route
7
+ # it to the non-deprecated name — identical output, no deprecation warning.
8
+ # https://github.com/sferik/multi_json/blob/v1.21.1/CHANGELOG.md#deprecated
9
+ module Json
10
+ ParseError = ::MultiJSON::ParseError
11
+
12
+ class << self
13
+ def dump(object)
14
+ ::MultiJSON.generate(object)
15
+ end
16
+
17
+ # parse is not deprecated; it's re-exposed (not renamed) because this
18
+ # facade is its own module and no longer inherits MultiJSON's methods.
19
+ def parse(source)
20
+ ::MultiJSON.parse(source)
21
+ end
22
+ end
23
+ end
24
+ elsif defined?(::MultiJson)
25
+ # Legacy multi_json (< 1.21) predates generate/parse and only exposes
26
+ # dump/load. Map Grape's surface onto them so the call sites stay
27
+ # engine-agnostic (these names are not deprecated on < 1.21).
28
+ module Json
29
+ # Mutually exclusive with the MultiJSON branch above; only one runs.
30
+ ParseError = ::MultiJson::ParseError # rubocop:disable Lint/ConstantReassignment
31
+
32
+ class << self
33
+ def dump(object)
34
+ ::MultiJson.dump(object)
35
+ end
36
+
37
+ def parse(source)
38
+ ::MultiJson.load(source)
39
+ end
40
+ end
41
+ end
6
42
  else
7
43
  Json = ::JSON
8
44
  Json::ParseError = Json::ParserError
@@ -11,6 +11,7 @@ en:
11
11
  exactly_one: 'are missing, exactly one parameter must be provided'
12
12
  except_values: 'has a value not allowed'
13
13
  incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
14
+ internal_server_error: 'Internal Server Error'
14
15
  invalid_accept_header:
15
16
  problem: 'invalid accept header'
16
17
  resolution: '%{message}'
@@ -42,6 +43,7 @@ en:
42
43
  resolution: 'eg: version ''v1'', using: :header, vendor: ''twitter'''
43
44
  summary: 'when version using header, you must specify :vendor option'
44
45
  mutual_exclusion: 'are mutually exclusive'
46
+ oneof: 'does not match any of the allowed schemas'
45
47
  presence: 'is missing'
46
48
  regexp: 'is invalid'
47
49
  same_as: 'is not the same as %{parameter}'
@@ -8,9 +8,8 @@ module Grape
8
8
  super
9
9
  return unless options.key?(:type)
10
10
 
11
- @auth_strategy = Grape::Middleware::Auth::Strategies[options[:type]].tap do |auth_strategy|
12
- raise Grape::Exceptions::UnknownAuthStrategy.new(strategy: options[:type]) unless auth_strategy
13
- end
11
+ @auth_strategy = Grape::Middleware::Auth::Strategies[options[:type]]
12
+ raise Grape::Exceptions::UnknownAuthStrategy.new(strategy: options[:type]) unless @auth_strategy
14
13
  end
15
14
 
16
15
  def call!(env)
@@ -4,24 +4,27 @@ module Grape
4
4
  module Middleware
5
5
  module Auth
6
6
  module DSL
7
- def auth(type = nil, options = {}, &block)
7
+ def auth(type = nil, *legacy_options, **options, &block)
8
8
  namespace_inheritable = inheritable_setting.namespace_inheritable
9
9
  return namespace_inheritable[:auth] unless type
10
10
 
11
- namespace_inheritable[:auth] = options.reverse_merge(type: type.to_sym, proc: block)
11
+ options = merge_legacy_auth_options(:auth, legacy_options, options)
12
+ namespace_inheritable[:auth] = { type: type.to_sym, proc: block }.merge!(options)
12
13
  use Grape::Middleware::Auth::Base, namespace_inheritable[:auth]
13
14
  end
14
15
 
15
16
  # Add HTTP Basic authorization to the API.
16
17
  #
17
- # @param [Hash] options A hash of options.
18
- # @option options [String] :realm "API Authorization" The HTTP Basic realm.
19
- def http_basic(options = {}, &)
18
+ # @param options [Hash] a hash of options
19
+ # @option options [String] :realm "API Authorization" the HTTP Basic realm
20
+ def http_basic(*legacy_options, **options, &)
21
+ options = merge_legacy_auth_options(:http_basic, legacy_options, options)
20
22
  options[:realm] ||= 'API Authorization'
21
- auth(:http_basic, options, &)
23
+ auth(:http_basic, **options, &)
22
24
  end
23
25
 
24
- def http_digest(options = {}, &)
26
+ def http_digest(*legacy_options, **options, &)
27
+ options = merge_legacy_auth_options(:http_digest, legacy_options, options)
25
28
  options[:realm] ||= 'API Authorization'
26
29
 
27
30
  if options[:realm].respond_to?(:values_at)
@@ -30,7 +33,19 @@ module Grape
30
33
  options[:opaque] ||= 'secret'
31
34
  end
32
35
 
33
- auth(:http_digest, options, &)
36
+ auth(:http_digest, **options, &)
37
+ end
38
+
39
+ private
40
+
41
+ # @deprecated Passing a positional options Hash is deprecated; pass
42
+ # keyword arguments instead. Kept so downstream callers keep working
43
+ # through the deprecation cycle.
44
+ def merge_legacy_auth_options(method_name, legacy_options, options)
45
+ return options if legacy_options.empty?
46
+
47
+ Grape.deprecator.warn("Passing a positional options Hash to `#{method_name}` is deprecated. Pass keyword arguments instead.")
48
+ legacy_options.first.merge(options)
34
49
  end
35
50
  end
36
51
  end
@@ -5,13 +5,25 @@ module Grape
5
5
  class Base
6
6
  include Grape::DSL::Headers
7
7
 
8
- attr_reader :app, :env, :options
8
+ attr_reader :app, :env, :options, :config
9
9
 
10
10
  # @param [Rack Application] app The standard argument for a Rack middleware.
11
- # @param [Hash] options A hash of options, simply stored for use by subclasses.
11
+ # @param [Hash] options Options forwarded to the subclass. When the
12
+ # subclass declares an `Options` Data class, the kwargs are routed
13
+ # through it and exposed via {#config}; {#options} keeps returning a
14
+ # frozen Hash representation for back-compat with subclasses that read
15
+ # `options[:key]`. Otherwise the kwargs are deep-merged with the
16
+ # subclass's `DEFAULT_OPTIONS` Hash (legacy path) and frozen.
12
17
  def initialize(app, **options)
13
18
  @app = app
14
- @options = merge_default_options(options)
19
+ if self.class.const_defined?(:Options)
20
+ # Search ancestors so subclasses (e.g. Versioner::Path → Versioner::Base)
21
+ # inherit their parent's Options Data class without redeclaring it.
22
+ @config = self.class::Options.new(**options)
23
+ @options = @config.to_h.freeze
24
+ else
25
+ @options = merge_default_options(options).freeze
26
+ end
15
27
  @app_response = nil
16
28
  end
17
29
 
@@ -61,22 +73,6 @@ module Grape
61
73
  @app_response = Rack::Response[*@app_response]
62
74
  end
63
75
 
64
- def content_types
65
- @content_types ||= Grape::ContentTypes.content_types_for(options[:content_types])
66
- end
67
-
68
- def mime_types
69
- @mime_types ||= Grape::ContentTypes.mime_types_for(content_types)
70
- end
71
-
72
- def content_type_for(format)
73
- content_types_indifferent_access[format]
74
- end
75
-
76
- def content_type
77
- content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || 'text/html'
78
- end
79
-
80
76
  def query_params
81
77
  rack_request.GET
82
78
  rescue *Grape::RACK_ERRORS
@@ -86,26 +82,19 @@ module Grape
86
82
  private
87
83
 
88
84
  def merge_headers(response)
89
- return unless headers.is_a?(Hash)
85
+ return if @header.blank?
90
86
 
91
87
  case response
92
- when Rack::Response then response.headers.merge!(headers)
93
- when Array then response[1].merge!(headers)
88
+ when Rack::Response then response.headers.merge!(@header)
89
+ when Array then response[1].merge!(@header)
94
90
  end
95
91
  end
96
92
 
97
- def content_types_indifferent_access
98
- @content_types_indifferent_access ||= content_types.with_indifferent_access
99
- end
100
-
101
93
  def merge_default_options(options)
102
- if respond_to?(:default_options)
103
- default_options.deep_merge(options)
104
- elsif self.class.const_defined?(:DEFAULT_OPTIONS)
105
- self.class::DEFAULT_OPTIONS.deep_merge(options)
106
- else
107
- options
108
- end
94
+ return default_options.deep_merge(options) if respond_to?(:default_options)
95
+ return self.class::DEFAULT_OPTIONS.deep_merge(options) if self.class.const_defined?(:DEFAULT_OPTIONS)
96
+
97
+ options
109
98
  end
110
99
 
111
100
  def try_scrub(obj)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Middleware
5
+ # Mixin for per-middleware +Options+ +Data+ classes that need to keep
6
+ # accepting legacy +data[:key]+ Hash-style access while nudging callers
7
+ # toward the named accessor. Emits a +Grape.deprecator+ warning then
8
+ # forwards to +public_send(key)+.
9
+ module DeprecatedOptionsHashAccess
10
+ def [](key)
11
+ Grape.deprecator.warn(
12
+ "`#{self.class.name}#[]` is deprecated. " \
13
+ "Use the named accessor `#{key}` instead."
14
+ )
15
+ public_send(key) if members.include?(key)
16
+ end
17
+ end
18
+ end
19
+ end