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
@@ -17,12 +17,17 @@ module Grape
17
17
  # env['api.version'] => 'v1'
18
18
  #
19
19
  class Path < Base
20
+ def initialize(app, **options)
21
+ super
22
+ @prefixes = [mount_path, Grape::Util::PathNormalizer.call(prefix)].select { |p| p.present? && p != '/' }.freeze
23
+ end
24
+
20
25
  def before
21
- path_info = Grape::Router.normalize_path(env[Rack::PATH_INFO])
26
+ path_info = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
22
27
  return if path_info == '/'
23
28
 
24
- [mount_path, Grape::Router.normalize_path(prefix)].each do |path|
25
- path_info = path_info.delete_prefix(path) if path.present? && path != '/' && path_info.start_with?(path)
29
+ path_info = @prefixes.reduce(path_info) do |pi, path|
30
+ pi.start_with?(path) ? pi.delete_prefix(path) : pi
26
31
  end
27
32
 
28
33
  slash_position = path_info.index('/', 1) # omit the first one
@@ -23,13 +23,13 @@ module Grape
23
23
  settings&.map(&:space)
24
24
  end
25
25
 
26
- def eql?(other)
26
+ def ==(other)
27
27
  other.class == self.class &&
28
28
  other.space == space &&
29
29
  other.requirements == requirements &&
30
30
  other.options == options
31
31
  end
32
- alias == eql?
32
+ alias eql? ==
33
33
 
34
34
  def hash
35
35
  [self.class, space, requirements, options].hash
@@ -45,7 +45,7 @@ module Grape
45
45
  def initialize
46
46
  super
47
47
  @cache = Hash.new do |h, joined_space|
48
- h[joined_space] = Grape::Router.normalize_path(joined_space.join('/'))
48
+ h[joined_space] = Grape::Util::PathNormalizer.call(joined_space.join('/'))
49
49
  end
50
50
  end
51
51
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module ParamsBuilder
5
5
  class HashWithIndifferentAccess < Base
6
6
  def self.call(params)
7
- params.with_indifferent_access
7
+ ActiveSupport::HashWithIndifferentAccess.new(params)
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module Parser
5
5
  class Json < Base
6
6
  def self.call(object, _env)
7
- ::Grape::Json.load(object)
7
+ ::Grape::Json.parse(object)
8
8
  rescue ::Grape::Json::ParseError
9
9
  # handle JSON parsing errors via the rescue handlers or provide error message
10
10
  raise Grape::Exceptions::InvalidMessageBody.new('application/json')
data/lib/grape/path.rb CHANGED
@@ -21,23 +21,20 @@ module Grape
21
21
  private
22
22
 
23
23
  def build_suffix(raw_path, raw_namespace, settings)
24
- if uses_specific_format?(settings)
25
- "(.#{settings[:format]})"
26
- elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
27
- NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
28
- else
29
- DEFAULT_FORMAT_SEGMENT
30
- end
24
+ return "(.#{settings[:format]})" if uses_specific_format?(settings)
25
+ return NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT if !uses_path_versioning?(settings) || valid_part?(raw_namespace) || valid_part?(raw_path)
26
+
27
+ DEFAULT_FORMAT_SEGMENT
31
28
  end
32
29
 
33
30
  def build_parts(raw_path, raw_namespace, settings)
34
- [].tap do |parts|
35
- add_part(parts, settings[:mount_path])
36
- add_part(parts, settings[:root_prefix])
37
- parts << VERSION_SEGMENT if uses_path_versioning?(settings)
38
- add_part(parts, raw_namespace)
39
- add_part(parts, raw_path)
40
- end
31
+ parts = []
32
+ add_part(parts, settings[:mount_path])
33
+ add_part(parts, settings[:root_prefix])
34
+ parts << VERSION_SEGMENT if uses_path_versioning?(settings)
35
+ add_part(parts, raw_namespace)
36
+ add_part(parts, raw_path)
37
+ parts
41
38
  end
42
39
 
43
40
  def add_part(parts, value)
@@ -55,9 +52,9 @@ module Grape
55
52
  end
56
53
 
57
54
  def uses_path_versioning?(settings)
58
- return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
55
+ return false unless settings.key?(:version) && settings[:version_options]
59
56
 
60
- settings[:version] && settings[:version_options][:using] == :path
57
+ settings[:version] && settings[:version_options].using == :path
61
58
  end
62
59
 
63
60
  def valid_part?(part)
@@ -68,7 +65,7 @@ module Grape
68
65
  def initialize
69
66
  super
70
67
  @cache = Hash.new do |h, parts|
71
- h[parts] = Grape::Router.normalize_path(parts.join('/'))
68
+ h[parts] = Grape::Util::PathNormalizer.call(parts.join('/'))
72
69
  end
73
70
  end
74
71
  end
data/lib/grape/request.rb CHANGED
@@ -132,8 +132,8 @@ module Grape
132
132
  X-UA-Compatible
133
133
  X-WebKit-CS
134
134
  X-XSS-Protection
135
- ].each_with_object({}) do |header, response|
136
- response["HTTP_#{header.upcase.tr('-', '_')}"] = header
135
+ ].to_h do |header|
136
+ ["HTTP_#{header.upcase.tr('-', '_')}", header]
137
137
  end.freeze
138
138
 
139
139
  alias rack_params params
@@ -153,19 +153,26 @@ module Grape
153
153
  end
154
154
 
155
155
  def cookies
156
- @cookies ||= Grape::Cookies.new(-> { rack_cookies })
156
+ @cookies ||= Grape::Cookies.new(rack_cookies)
157
157
  end
158
158
 
159
- # needs to be public until extensions param_builder are removed
160
- def grape_routing_args
161
- # preserve version from query string parameters
162
- env[Grape::Env::GRAPE_ROUTING_ARGS]&.except(:version, :route_info) || {}
159
+ # True once the cookie jar has been materialized (a cookie was read or
160
+ # written this request). Lets the endpoint skip response-cookie flushing,
161
+ # and the Grape::Cookies allocation it triggers, when no cookie was
162
+ # touched on the request.
163
+ def cookies?
164
+ !@cookies.nil?
163
165
  end
164
166
 
165
167
  private
166
168
 
167
169
  def make_params
168
- @params_builder.call(rack_params).deep_merge!(grape_routing_args)
170
+ params = @params_builder.call(rack_params)
171
+ routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
172
+ filtered = routing_args&.except(:version, :route_info)
173
+ return params if filtered.blank?
174
+
175
+ params.deep_merge!(filtered)
169
176
  rescue *Grape::RACK_ERRORS
170
177
  raise Grape::Exceptions::RequestError
171
178
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ class Router
5
+ # Grape-style path patterns for Mustermann: `:param`, `*splat`, `{name}` /
6
+ # `{+splat}`, `( )` optionals, `|`, and an Integer digit-only constraint
7
+ # (driven by Grape's `params` option).
8
+ #
9
+ # Inlined from the mustermann-grape gem (MIT) by namusyaka, Konstantin Haase
10
+ # and Daniel Doubrovkine. Grape instantiates this class directly (see
11
+ # {Grape::Router::Pattern}), so unlike the gem it is not registered as a
12
+ # Mustermann `type: :grape`.
13
+ class MustermannPattern < ::Mustermann::AST::Pattern
14
+ supported_options :params
15
+
16
+ on(nil, '?', ')') { |c| unexpected(c) }
17
+
18
+ on('*') { |_c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
19
+ on(':') do |_c|
20
+ param_name = scan(/\w+/)
21
+ # Integer params (declared via Grape's `params` option) match digits only;
22
+ # any other capture matches a single path segment (anything but / ? # .).
23
+ param_type = pattern&.options&.dig(:params, param_name, :type)
24
+ constraint = param_type == 'Integer' ? /\d/ : '[^/?#.]'
25
+ node(:capture, param_name, constraint:) { scan(/\w+/) }
26
+ end
27
+ on('\\') { |_c| node(:char, expect(/./)) }
28
+ on('(') { |_c| node(:optional, node(:group) { read unless scan(')') }) }
29
+ on('|') { |_c| node(:or) }
30
+
31
+ on('{') do |_c|
32
+ type = scan('+') ? :named_splat : :capture
33
+ name = expect(/[\w.]+/)
34
+ type = :splat if (type == :named_splat) && (name == 'splat')
35
+ expect('}')
36
+ node(type, name)
37
+ end
38
+
39
+ suffix('?') do |_c, element|
40
+ node(:optional, element)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -16,7 +16,7 @@ module Grape
16
16
  def initialize(origin:, suffix:, anchor:, params:, format:, version:, requirements:)
17
17
  @origin = origin
18
18
  @path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
19
- @pattern = Mustermann::Grape.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements))
19
+ @pattern = MustermannPattern.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements))
20
20
  @to_regexp = @pattern.to_regexp
21
21
  end
22
22
 
@@ -39,15 +39,11 @@ module Grape
39
39
  end
40
40
 
41
41
  def build_path_from_pattern(pattern, anchor)
42
- if pattern.end_with?('*path')
43
- pattern.dup.insert(pattern.rindex('/') + 1, '?')
44
- elsif anchor
45
- pattern
46
- elsif pattern.end_with?('/')
47
- "#{pattern}?*path"
48
- else
49
- "#{pattern}/?*path"
50
- end
42
+ return pattern.dup.insert(pattern.rindex('/') + 1, '?') if pattern.end_with?('*path')
43
+ return pattern if anchor
44
+ return "#{pattern}?*path" if pattern.end_with?('/')
45
+
46
+ "#{pattern}/?*path"
51
47
  end
52
48
 
53
49
  def map_str(value)
data/lib/grape/router.rb CHANGED
@@ -2,31 +2,12 @@
2
2
 
3
3
  module Grape
4
4
  class Router
5
- # Taken from Rails
6
- # normalize_path("/foo") # => "/foo"
7
- # normalize_path("/foo/") # => "/foo"
8
- # normalize_path("foo") # => "/foo"
9
- # normalize_path("") # => "/"
10
- # normalize_path("/%ab") # => "/%AB"
11
- # https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19
5
+ # @deprecated Use {Grape::Util::PathNormalizer.call} instead.
12
6
  def self.normalize_path(path)
13
- return '/' unless path
14
- return path if path == '/'
15
-
16
- # Fast path for the overwhelming majority of paths that don't need to be normalized
17
- return path if path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//}))
18
-
19
- # Slow path
20
- encoding = path.encoding
21
- path = "/#{path}"
22
- path.squeeze!('/')
23
-
24
- unless path == '/'
25
- path.delete_suffix!('/')
26
- path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
27
- end
28
-
29
- path.force_encoding(encoding)
7
+ Grape.deprecator.warn(
8
+ '`Grape::Router.normalize_path` is deprecated. Use `Grape::Util::PathNormalizer.call` instead.'
9
+ )
10
+ Grape::Util::PathNormalizer.call(path)
30
11
  end
31
12
 
32
13
  def initialize
@@ -62,7 +43,7 @@ module Grape
62
43
 
63
44
  def call(env)
64
45
  with_optimization do
65
- input = Router.normalize_path(env[Rack::PATH_INFO])
46
+ input = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
66
47
  method = env[Rack::REQUEST_METHOD]
67
48
  response, route = identity(input, method, env)
68
49
  response || rotation(input, method, env, route)
@@ -103,20 +84,10 @@ module Grape
103
84
  end
104
85
 
105
86
  def transaction(input, method, env)
106
- # using a Proc is important since `return` will exit the enclosing function
107
- cascade_or_return_response = proc do |response|
108
- if response
109
- cascade?(response).tap do |cascade|
110
- return response unless cascade
111
-
112
- # we need to close the body if possible before dismissing
113
- response[2].close if response[2].respond_to?(:close)
114
- end
115
- end
116
- end
117
-
118
87
  response = yield
119
- last_response_cascade = cascade_or_return_response.call(response)
88
+ return response if halt?(response)
89
+
90
+ last_response_cascade = !response.nil?
120
91
  last_neighbor_route = greedy_match?(input)
121
92
 
122
93
  # If last_neighbor_route exists and request method is OPTIONS,
@@ -127,18 +98,33 @@ module Grape
127
98
 
128
99
  return last_neighbor_route.call(env) if last_neighbor_route && last_response_cascade && route
129
100
 
130
- last_response_cascade = cascade_or_return_response.call(process_route(route, input, env)) if route
101
+ if route
102
+ route_response = process_route(route, input, env)
103
+ return route_response if halt?(route_response)
104
+
105
+ last_response_cascade = !route_response.nil?
106
+ end
131
107
 
132
108
  return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && last_neighbor_route
133
109
 
134
110
  nil
135
111
  end
136
112
 
113
+ # Returns true if `response` should be returned as-is from the enclosing
114
+ # transaction. Closes the body as a side effect when the response is
115
+ # cascading so callers can safely try the next match.
116
+ def halt?(response)
117
+ return false unless response
118
+
119
+ cascade = cascade?(response)
120
+ response[2].close if cascade && response[2].respond_to?(:close)
121
+ !cascade
122
+ end
123
+
137
124
  def process_route(route, input, env, include_allow_header: false)
138
- args = env[Grape::Env::GRAPE_ROUTING_ARGS] || { route_info: route }
139
125
  route_params = route.params(input)
140
- routing_args = args.merge(route_params || {})
141
- env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args
126
+ env[Grape::Env::GRAPE_ROUTING_ARGS] ||= { route_info: route }
127
+ env[Grape::Env::GRAPE_ROUTING_ARGS].merge!(route_params) if route_params.present?
142
128
  env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
143
129
  route.call(env)
144
130
  end
@@ -31,6 +31,7 @@ module Grape
31
31
  def ==(other)
32
32
  path == other.path
33
33
  end
34
+ alias eql? ==
34
35
  end
35
36
  end
36
37
  end
@@ -6,11 +6,9 @@ module Grape
6
6
  # for using Rack::SendFile middleware
7
7
  class SendfileResponse < Rack::Response
8
8
  def respond_to?(method_name, include_all = false)
9
- if method_name == :to_path
10
- @body.respond_to?(:to_path, include_all)
11
- else
12
- super
13
- end
9
+ return @body.respond_to?(:to_path, include_all) if method_name == :to_path
10
+
11
+ super
14
12
  end
15
13
 
16
14
  def to_path
@@ -18,6 +18,7 @@ module Grape
18
18
  def ==(other)
19
19
  stream == other.stream
20
20
  end
21
+ alias eql? ==
21
22
  end
22
23
  end
23
24
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Testing
5
+ module RunBeforeEach
6
+ def run
7
+ self.class.run_before_each(self)
8
+ super
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def before_each(&block)
14
+ raise ArgumentError, 'a block is required' unless block
15
+
16
+ @before_each ||= []
17
+ @before_each << block
18
+ end
19
+
20
+ def reset_before_each
21
+ @before_each&.clear
22
+ end
23
+
24
+ def run_before_each(endpoint)
25
+ superclass.run_before_each(endpoint) unless self == Grape::Endpoint
26
+ @before_each&.each { |blk| blk.call(endpoint) }
27
+ end
28
+ end
29
+
30
+ Grape::Endpoint.prepend(RunBeforeEach)
31
+ Grape::Endpoint.extend(ClassMethods)
32
+ end
33
+ end
@@ -4,6 +4,9 @@ module Grape
4
4
  module Util
5
5
  # Base for classes which need to operate with own values kept
6
6
  # in the hash and inherited values kept in a Hash-like object.
7
+ #
8
+ # +@new_values+ is lazily allocated on first write so settings layers
9
+ # that only inherit (never override) don't carry an empty Hash each.
7
10
  class BaseInheritable
8
11
  attr_accessor :inherited_values, :new_values
9
12
 
@@ -11,35 +14,29 @@ module Grape
11
14
  # of the Hash class.
12
15
  def initialize(inherited_values = nil)
13
16
  @inherited_values = inherited_values || {}
14
- @new_values = {}
17
+ # @new_values stays nil until the first write.
15
18
  end
16
19
 
17
20
  def delete(*keys)
18
- keys.map do |key|
19
- # since delete returns the deleted value, seems natural to `map` the result
20
- new_values.delete key
21
- end
21
+ return [] unless @new_values
22
+
23
+ keys.map { |key| @new_values.delete(key) }
22
24
  end
23
25
 
24
26
  def initialize_copy(other)
25
27
  super
26
- self.inherited_values = other.inherited_values
27
- self.new_values = other.new_values.dup
28
+ @inherited_values = other.inherited_values
29
+ @new_values = other.new_values&.dup
28
30
  end
29
31
 
30
32
  def keys
31
- if new_values.any?
32
- inherited_values.keys.tap do |combined|
33
- combined.concat(new_values.keys)
34
- combined.uniq!
35
- end
36
- else
37
- inherited_values.keys
38
- end
33
+ return @inherited_values.keys if @new_values.nil? || @new_values.empty?
34
+
35
+ (@inherited_values.keys + @new_values.keys).uniq
39
36
  end
40
37
 
41
38
  def key?(name)
42
- inherited_values.key?(name) || new_values.key?(name)
39
+ @inherited_values.key?(name) || @new_values&.key?(name) || false
43
40
  end
44
41
  end
45
42
  end
@@ -5,7 +5,17 @@ module Grape
5
5
  # A branchable, inheritable settings object which can store both stackable
6
6
  # and inheritable values (see InheritableValues and StackableValues).
7
7
  class InheritableSetting
8
- attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent, :point_in_time_copies
8
+ attr_reader :route, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent
9
+
10
+ # Lazy-allocated; +api_class+ and +point_in_time_copies+ are rarely
11
+ # written on most settings layers, so don't pay for a Hash/Array each.
12
+ def api_class
13
+ @api_class ||= {}
14
+ end
15
+
16
+ def point_in_time_copies
17
+ @point_in_time_copies ||= []
18
+ end
9
19
 
10
20
  # Retrieve global settings.
11
21
  def self.global
@@ -23,17 +33,14 @@ module Grape
23
33
  # instance can then be set to inherit from an existing instance (see
24
34
  # #inherit_from).
25
35
  def initialize
26
- self.route = {}
27
- self.api_class = {}
28
- self.namespace = InheritableValues.new # only inheritable from a parent when
36
+ @route = {}
37
+ @namespace = InheritableValues.new # only inheritable from a parent when
29
38
  # used with a mount, or should every API::Class be a separate namespace by default?
30
- self.namespace_inheritable = InheritableValues.new
31
- self.namespace_stackable = StackableValues.new
32
- self.namespace_reverse_stackable = ReverseStackableValues.new
33
-
34
- self.point_in_time_copies = []
35
-
36
- self.parent = nil
39
+ @namespace_inheritable = InheritableValues.new
40
+ @namespace_stackable = StackableValues.new
41
+ @namespace_reverse_stackable = ReverseStackableValues.new
42
+ @parent = nil
43
+ # @api_class and @point_in_time_copies stay nil until first access.
37
44
  end
38
45
 
39
46
  # Return the class-level global properties.
@@ -48,14 +55,14 @@ module Grape
48
55
  def inherit_from(parent)
49
56
  return if parent.nil?
50
57
 
51
- self.parent = parent
58
+ @parent = parent
52
59
 
53
60
  namespace_inheritable.inherited_values = parent.namespace_inheritable
54
61
  namespace_stackable.inherited_values = parent.namespace_stackable
55
62
  namespace_reverse_stackable.inherited_values = parent.namespace_reverse_stackable
56
- self.route = parent.route.merge(route)
63
+ @route = parent.route.merge(route)
57
64
 
58
- point_in_time_copies.each { |cloned_one| cloned_one.inherit_from parent }
65
+ @point_in_time_copies&.each { |cloned_one| cloned_one.inherit_from parent }
59
66
  end
60
67
 
61
68
  # Create a point-in-time copy of this settings instance, with clones of
@@ -63,19 +70,11 @@ module Grape
63
70
  # changed via #inherit_from, it will copy that inheritence to any copies
64
71
  # which were made.
65
72
  def point_in_time_copy
66
- self.class.new.tap do |new_setting|
67
- point_in_time_copies << new_setting
68
- new_setting.point_in_time_copies = []
69
-
70
- new_setting.namespace = namespace.clone
71
- new_setting.namespace_inheritable = namespace_inheritable.clone
72
- new_setting.namespace_stackable = namespace_stackable.clone
73
- new_setting.namespace_reverse_stackable = namespace_reverse_stackable.clone
74
- new_setting.route = route.clone
75
- new_setting.api_class = api_class
76
-
77
- new_setting.inherit_from(parent)
78
- end
73
+ new_setting = self.class.new
74
+ point_in_time_copies << new_setting
75
+ new_setting.copy_state_from(self)
76
+ new_setting.inherit_from(parent)
77
+ new_setting
79
78
  end
80
79
 
81
80
  # Resets the instance store of per-route settings.
@@ -96,12 +95,30 @@ module Grape
96
95
  }
97
96
  end
98
97
 
98
+ def ==(other)
99
+ other.is_a?(self.class) && to_hash == other.to_hash
100
+ end
101
+ alias eql? ==
102
+
99
103
  def namespace_stackable_with_hash(key)
100
104
  data = namespace_stackable[key]
101
105
  return if data.blank?
102
106
 
103
107
  data.each_with_object({}) { |value, result| result.deep_merge!(value) }
104
108
  end
109
+
110
+ protected
111
+
112
+ # Used by +point_in_time_copy+ to populate a freshly-built instance
113
+ # with cloned state from another instance of the same class.
114
+ def copy_state_from(source)
115
+ @namespace = source.namespace.clone
116
+ @namespace_inheritable = source.namespace_inheritable.clone
117
+ @namespace_stackable = source.namespace_stackable.clone
118
+ @namespace_reverse_stackable = source.namespace_reverse_stackable.clone
119
+ @route = source.route.clone
120
+ @api_class = source.api_class
121
+ end
105
122
  end
106
123
  end
107
124
  end
@@ -4,11 +4,13 @@ module Grape
4
4
  module Util
5
5
  class InheritableValues < BaseInheritable
6
6
  def [](name)
7
- values[name]
7
+ return @inherited_values[name] unless @new_values
8
+
9
+ @new_values.fetch(name) { @inherited_values[name] }
8
10
  end
9
11
 
10
12
  def []=(name, value)
11
- new_values[name] = value
13
+ (@new_values ||= {})[name] = value
12
14
  end
13
15
 
14
16
  def merge(new_hash)
@@ -22,7 +24,9 @@ module Grape
22
24
  protected
23
25
 
24
26
  def values
25
- @inherited_values.merge(@new_values)
27
+ return @inherited_values.merge(@new_values) if @new_values && !@new_values.empty?
28
+
29
+ @inherited_values.is_a?(Hash) ? @inherited_values.dup : @inherited_values.to_hash
26
30
  end
27
31
  end
28
32
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ # Abstract parent for lazy wrappers used by the remount/configuration
7
+ # machinery. Call sites can type-check with +is_a?(Grape::Util::Lazy::Base)+
8
+ # instead of enumerating the concrete subclasses.
9
+ class Base
10
+ def to_s
11
+ evaluate.to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -3,8 +3,9 @@
3
3
  module Grape
4
4
  module Util
5
5
  module Lazy
6
- class Block
6
+ class Block < Base
7
7
  def initialize(&new_block)
8
+ super()
8
9
  @block = new_block
9
10
  end
10
11
 
@@ -15,14 +16,6 @@ module Grape
15
16
  def evaluate
16
17
  @block.call({})
17
18
  end
18
-
19
- def lazy?
20
- true
21
- end
22
-
23
- def to_s
24
- evaluate.to_s
25
- end
26
19
  end
27
20
  end
28
21
  end