grape 2.0.0 → 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/README.md +362 -316
  4. data/UPGRADING.md +197 -7
  5. data/grape.gemspec +5 -6
  6. data/lib/grape/api/instance.rb +14 -11
  7. data/lib/grape/api.rb +19 -10
  8. data/lib/grape/content_types.rb +0 -2
  9. data/lib/grape/cookies.rb +2 -1
  10. data/lib/grape/dry_types.rb +0 -2
  11. data/lib/grape/dsl/desc.rb +22 -20
  12. data/lib/grape/dsl/headers.rb +1 -1
  13. data/lib/grape/dsl/inside_route.rb +46 -15
  14. data/lib/grape/dsl/parameters.rb +5 -4
  15. data/lib/grape/dsl/routing.rb +20 -4
  16. data/lib/grape/dsl/validations.rb +13 -0
  17. data/lib/grape/endpoint.rb +14 -17
  18. data/lib/grape/{util/env.rb → env.rb} +0 -5
  19. data/lib/grape/error_formatter/txt.rb +11 -10
  20. data/lib/grape/exceptions/base.rb +3 -3
  21. data/lib/grape/exceptions/validation.rb +0 -2
  22. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  23. data/lib/grape/exceptions/validation_errors.rb +2 -4
  24. data/lib/grape/extensions/hash.rb +5 -1
  25. data/lib/grape/http/headers.rb +18 -34
  26. data/lib/grape/{util/json.rb → json.rb} +1 -3
  27. data/lib/grape/locale/en.yml +3 -0
  28. data/lib/grape/middleware/auth/base.rb +0 -2
  29. data/lib/grape/middleware/auth/dsl.rb +0 -2
  30. data/lib/grape/middleware/base.rb +1 -3
  31. data/lib/grape/middleware/error.rb +55 -50
  32. data/lib/grape/middleware/formatter.rb +16 -13
  33. data/lib/grape/middleware/globals.rb +1 -3
  34. data/lib/grape/middleware/stack.rb +4 -5
  35. data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
  36. data/lib/grape/middleware/versioner/header.rb +17 -163
  37. data/lib/grape/middleware/versioner/param.rb +2 -4
  38. data/lib/grape/middleware/versioner/path.rb +1 -3
  39. data/lib/grape/namespace.rb +3 -4
  40. data/lib/grape/path.rb +24 -29
  41. data/lib/grape/request.rb +4 -12
  42. data/lib/grape/router/base_route.rb +39 -0
  43. data/lib/grape/router/greedy_route.rb +20 -0
  44. data/lib/grape/router/pattern.rb +39 -30
  45. data/lib/grape/router/route.rb +22 -59
  46. data/lib/grape/router.rb +32 -37
  47. data/lib/grape/util/accept_header_handler.rb +105 -0
  48. data/lib/grape/util/base_inheritable.rb +4 -4
  49. data/lib/grape/util/cache.rb +0 -3
  50. data/lib/grape/util/endpoint_configuration.rb +1 -1
  51. data/lib/grape/util/header.rb +13 -0
  52. data/lib/grape/util/inheritable_values.rb +0 -2
  53. data/lib/grape/util/lazy/block.rb +29 -0
  54. data/lib/grape/util/lazy/object.rb +45 -0
  55. data/lib/grape/util/lazy/value.rb +38 -0
  56. data/lib/grape/util/lazy/value_array.rb +21 -0
  57. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  58. data/lib/grape/util/lazy/value_hash.rb +21 -0
  59. data/lib/grape/util/media_type.rb +70 -0
  60. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  61. data/lib/grape/util/stackable_values.rb +1 -6
  62. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  63. data/lib/grape/validations/attributes_doc.rb +38 -36
  64. data/lib/grape/validations/attributes_iterator.rb +1 -0
  65. data/lib/grape/validations/contract_scope.rb +71 -0
  66. data/lib/grape/validations/params_scope.rb +22 -19
  67. data/lib/grape/validations/types/array_coercer.rb +0 -2
  68. data/lib/grape/validations/types/build_coercer.rb +69 -71
  69. data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
  70. data/lib/grape/validations/types/json.rb +0 -2
  71. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  72. data/lib/grape/validations/types/set_coercer.rb +0 -3
  73. data/lib/grape/validations/types.rb +0 -3
  74. data/lib/grape/validations/validators/base.rb +1 -0
  75. data/lib/grape/validations/validators/default_validator.rb +5 -1
  76. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  77. data/lib/grape/validations/validators/length_validator.rb +42 -0
  78. data/lib/grape/validations/validators/values_validator.rb +6 -1
  79. data/lib/grape/validations.rb +3 -7
  80. data/lib/grape/version.rb +1 -1
  81. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  82. data/lib/grape.rb +30 -274
  83. metadata +30 -37
  84. data/lib/grape/eager_load.rb +0 -20
  85. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  86. data/lib/grape/router/attribute_translator.rb +0 -63
  87. data/lib/grape/util/lazy_block.rb +0 -27
  88. data/lib/grape/util/lazy_object.rb +0 -43
  89. data/lib/grape/util/lazy_value.rb +0 -91
@@ -1,57 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router/pattern'
4
- require 'grape/router/attribute_translator'
5
- require 'forwardable'
6
- require 'pathname'
7
-
8
3
  module Grape
9
4
  class Router
10
- class Route
11
- ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/.freeze
12
- SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze
13
- FIXED_NAMED_CAPTURES = %w[format version].freeze
14
-
15
- attr_accessor :pattern, :translator, :app, :index, :options
16
-
17
- alias attributes translator
18
-
5
+ class Route < BaseRoute
19
6
  extend Forwardable
20
- def_delegators :pattern, :path, :origin
21
- delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
22
7
 
23
- def method_missing(method_id, *arguments)
24
- match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
25
- if match
26
- method_name = match.captures.last.to_sym
27
- warn_route_methods(method_name, caller(1).shift)
28
- @options[method_name]
29
- else
30
- super
31
- end
32
- end
33
-
34
- def respond_to_missing?(method_id, _)
35
- ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
36
- end
37
-
38
- def route_method
39
- warn_route_methods(:method, caller(1).shift, :request_method)
40
- request_method
41
- end
8
+ attr_reader :app, :request_method
42
9
 
43
- def route_path
44
- warn_route_methods(:path, caller(1).shift)
45
- pattern.path
46
- end
10
+ def_delegators :pattern, :path, :origin
47
11
 
48
12
  def initialize(method, pattern, **options)
49
- method_s = method.to_s
50
- method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase
51
-
52
- @options = options.merge(method: method_upcase)
53
- @pattern = Pattern.new(pattern, **options)
54
- @translator = AttributeTranslator.new(**options, request_method: method_upcase)
13
+ @request_method = upcase_method(method)
14
+ @pattern = Grape::Router::Pattern.new(pattern, **options)
15
+ super(**options)
55
16
  end
56
17
 
57
18
  def exec(env)
@@ -64,27 +25,29 @@ module Grape
64
25
  end
65
26
 
66
27
  def match?(input)
67
- translator.respond_to?(:forward_match) && translator.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input)
28
+ return false if input.blank?
29
+
30
+ options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
68
31
  end
69
32
 
70
33
  def params(input = nil)
71
- if input.nil?
72
- pattern.named_captures.keys.each_with_object(translator.params) do |(key), defaults|
73
- defaults[key] ||= '' unless FIXED_NAMED_CAPTURES.include?(key) || defaults.key?(key)
74
- end
75
- else
76
- parsed = pattern.params(input)
77
- parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {}
78
- end
34
+ return params_without_input if input.blank?
35
+
36
+ parsed = pattern.params(input)
37
+ return {} unless parsed
38
+
39
+ parsed.compact.symbolize_keys
79
40
  end
80
41
 
81
42
  private
82
43
 
83
- def warn_route_methods(name, location, expected = nil)
84
- path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
85
- path = File.realpath(path) if Pathname.new(path).relative?
86
- expected ||= name
87
- Grape.deprecator.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.")
44
+ def params_without_input
45
+ pattern.captures_default.merge(attributes.params)
46
+ end
47
+
48
+ def upcase_method(method)
49
+ method_s = method.to_s
50
+ Grape::Http::Headers::SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
88
51
  end
89
52
  end
90
53
  end
data/lib/grape/router.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router/route'
4
- require 'grape/util/cache'
5
-
6
3
  module Grape
7
4
  class Router
8
5
  attr_reader :map, :compiled
@@ -15,10 +12,6 @@ module Grape
15
12
  path
16
13
  end
17
14
 
18
- def self.supported_methods
19
- @supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
20
- end
21
-
22
15
  def initialize
23
16
  @neutral_map = []
24
17
  @neutral_regexes = []
@@ -31,13 +24,12 @@ module Grape
31
24
 
32
25
  @union = Regexp.union(@neutral_regexes)
33
26
  @neutral_regexes = nil
34
- self.class.supported_methods.each do |method|
27
+ (Grape::Http::Headers::SUPPORTED_METHODS + ['*']).each do |method|
28
+ next unless map.key?(method)
29
+
35
30
  routes = map[method]
36
- @optimized_map[method] = routes.map.with_index do |route, index|
37
- route.index = index
38
- Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
39
- end
40
- @optimized_map[method] = Regexp.union(@optimized_map[method])
31
+ optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
32
+ @optimized_map[method] = Regexp.union(optimized_map)
41
33
  end
42
34
  @compiled = true
43
35
  end
@@ -47,8 +39,10 @@ module Grape
47
39
  end
48
40
 
49
41
  def associate_routes(pattern, **options)
50
- @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
51
- @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length)
42
+ Grape::Router::GreedyRoute.new(pattern: pattern, **options).then do |greedy_route|
43
+ @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
44
+ @neutral_map << greedy_route
45
+ end
52
46
  end
53
47
 
54
48
  def call(env)
@@ -91,26 +85,33 @@ module Grape
91
85
 
92
86
  def transaction(env)
93
87
  input, method = *extract_input_and_method(env)
94
- response = yield(input, method)
95
88
 
96
- return response if response && !(cascade = cascade?(response))
89
+ # using a Proc is important since `return` will exit the enclosing function
90
+ cascade_or_return_response = proc do |response|
91
+ if response
92
+ cascade?(response).tap do |cascade|
93
+ return response unless cascade
97
94
 
95
+ # we need to close the body if possible before dismissing
96
+ response[2].close if response[2].respond_to?(:close)
97
+ end
98
+ end
99
+ end
100
+
101
+ last_response_cascade = cascade_or_return_response.call(yield(input, method))
98
102
  last_neighbor_route = greedy_match?(input)
99
103
 
100
104
  # If last_neighbor_route exists and request method is OPTIONS,
101
105
  # return response by using #call_with_allow_headers.
102
- return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade
106
+ return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Rack::OPTIONS && !last_response_cascade
103
107
 
104
108
  route = match?(input, '*')
105
109
 
106
- return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route
110
+ return last_neighbor_route.endpoint.call(env) if last_neighbor_route && last_response_cascade && route
107
111
 
108
- if route
109
- response = process_route(route, env)
110
- return response if response && !(cascade = cascade?(response))
111
- end
112
+ last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route
112
113
 
113
- return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route
114
+ return call_with_allow_headers(env, last_neighbor_route) if !last_response_cascade && last_neighbor_route
114
115
 
115
116
  nil
116
117
  end
@@ -122,12 +123,12 @@ module Grape
122
123
 
123
124
  def make_routing_args(default_args, route, input)
124
125
  args = default_args || { route_info: route }
125
- args.merge(route.params(input) || {})
126
+ args.merge(route.params(input))
126
127
  end
127
128
 
128
129
  def extract_input_and_method(env)
129
- input = string_for(env[Grape::Http::Headers::PATH_INFO])
130
- method = env[Grape::Http::Headers::REQUEST_METHOD]
130
+ input = string_for(env[Rack::PATH_INFO])
131
+ method = env[Rack::REQUEST_METHOD]
131
132
  [input, method]
132
133
  end
133
134
 
@@ -137,22 +138,16 @@ module Grape
137
138
  end
138
139
 
139
140
  def default_response
140
- [404, { Grape::Http::Headers::X_CASCADE => 'pass' }, ['404 Not Found']]
141
+ headers = Grape::Util::Header.new.merge(Grape::Http::Headers::X_CASCADE => 'pass')
142
+ [404, headers, ['404 Not Found']]
141
143
  end
142
144
 
143
145
  def match?(input, method)
144
- current_regexp = @optimized_map[method]
145
- return unless current_regexp.match(input)
146
-
147
- last_match = Regexp.last_match
148
- @map[method].detect { |route| last_match["_#{route.index}"] }
146
+ @optimized_map[method].match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_index] } }
149
147
  end
150
148
 
151
149
  def greedy_match?(input)
152
- return unless @union.match(input)
153
-
154
- last_match = Regexp.last_match
155
- @neutral_map.detect { |route| last_match["_#{route.index}"] }
150
+ @union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_index] } }
156
151
  end
157
152
 
158
153
  def call_with_allow_headers(env, route)
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ class AcceptHeaderHandler
6
+ attr_reader :accept_header, :versions, :vendor, :strict, :cascade
7
+
8
+ def initialize(accept_header:, versions:, **options)
9
+ @accept_header = accept_header
10
+ @versions = versions
11
+ @vendor = options.fetch(:vendor, nil)
12
+ @strict = options.fetch(:strict, false)
13
+ @cascade = options.fetch(:cascade, true)
14
+ end
15
+
16
+ def match_best_quality_media_type!(content_types: Grape::ContentTypes::CONTENT_TYPES, allowed_methods: nil)
17
+ return unless vendor
18
+
19
+ strict_header_checks!
20
+ media_type = Grape::Util::MediaType.best_quality(accept_header, available_media_types(content_types))
21
+ if media_type
22
+ yield media_type
23
+ else
24
+ fail!(allowed_methods)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def strict_header_checks!
31
+ return unless strict
32
+
33
+ accept_header_check!
34
+ version_and_vendor_check!
35
+ end
36
+
37
+ def accept_header_check!
38
+ return if accept_header.present?
39
+
40
+ invalid_accept_header!('Accept header must be set.')
41
+ end
42
+
43
+ def version_and_vendor_check!
44
+ return if versions.blank? || version_and_vendor?
45
+
46
+ invalid_accept_header!('API vendor or version not found.')
47
+ end
48
+
49
+ def q_values_mime_types
50
+ @q_values_mime_types ||= Rack::Utils.q_values(accept_header).map(&:first)
51
+ end
52
+
53
+ def version_and_vendor?
54
+ q_values_mime_types.any? { |mime_type| Grape::Util::MediaType.match?(mime_type) }
55
+ end
56
+
57
+ def invalid_accept_header!(message)
58
+ raise Grape::Exceptions::InvalidAcceptHeader.new(message, error_headers)
59
+ end
60
+
61
+ def invalid_version_header!(message)
62
+ raise Grape::Exceptions::InvalidVersionHeader.new(message, error_headers)
63
+ end
64
+
65
+ def fail!(grape_allowed_methods)
66
+ return grape_allowed_methods if grape_allowed_methods.present?
67
+
68
+ media_types = q_values_mime_types.map { |mime_type| Grape::Util::MediaType.parse(mime_type) }
69
+ vendor_not_found!(media_types) || version_not_found!(media_types)
70
+ end
71
+
72
+ def vendor_not_found!(media_types)
73
+ return unless media_types.all? { |media_type| media_type&.vendor && media_type.vendor != vendor }
74
+
75
+ invalid_accept_header!('API vendor not found.')
76
+ end
77
+
78
+ def version_not_found!(media_types)
79
+ return unless media_types.all? { |media_type| media_type&.version && versions.exclude?(media_type.version) }
80
+
81
+ invalid_version_header!('API version not found.')
82
+ end
83
+
84
+ def error_headers
85
+ cascade ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
86
+ end
87
+
88
+ def available_media_types(content_types)
89
+ [].tap do |available_media_types|
90
+ base_media_type = "application/vnd.#{vendor}"
91
+ content_types.each_key do |extension|
92
+ versions&.reverse_each do |version|
93
+ available_media_types << "#{base_media_type}-#{version}+#{extension}"
94
+ available_media_types << "#{base_media_type}-#{version}"
95
+ end
96
+ available_media_types << "#{base_media_type}+#{extension}"
97
+ end
98
+
99
+ available_media_types << base_media_type
100
+ available_media_types.concat(content_types.values.flatten)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -26,10 +26,10 @@ module Grape
26
26
 
27
27
  def keys
28
28
  if new_values.any?
29
- combined = inherited_values.keys
30
- combined.concat(new_values.keys)
31
- combined.uniq!
32
- combined
29
+ inherited_values.keys.tap do |combined|
30
+ combined.concat(new_values.keys)
31
+ combined.uniq!
32
+ end
33
33
  else
34
34
  inherited_values.keys
35
35
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
- require 'forwardable'
5
-
6
3
  module Grape
7
4
  module Util
8
5
  class Cache
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Grape
4
4
  module Util
5
- class EndpointConfiguration < LazyValueHash
5
+ class EndpointConfiguration < Lazy::ValueHash
6
6
  end
7
7
  end
8
8
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ if Gem::Version.new(Rack.release) >= Gem::Version.new('3')
6
+ require 'rack/headers'
7
+ Header = Rack::Headers
8
+ else
9
+ require 'rack/utils'
10
+ Header = Rack::Utils::HeaderHash
11
+ end
12
+ end
13
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base_inheritable'
4
-
5
3
  module Grape
6
4
  module Util
7
5
  class InheritableValues < BaseInheritable
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class Block
7
+ def initialize(&new_block)
8
+ @block = new_block
9
+ end
10
+
11
+ def evaluate_from(configuration)
12
+ @block.call(configuration)
13
+ end
14
+
15
+ def evaluate
16
+ @block.call({})
17
+ end
18
+
19
+ def lazy?
20
+ true
21
+ end
22
+
23
+ def to_s
24
+ evaluate.to_s
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Based on https://github.com/HornsAndHooves/lazy_object
4
+
5
+ module Grape
6
+ module Util
7
+ module Lazy
8
+ class Object < BasicObject
9
+ attr_reader :callable
10
+
11
+ def initialize(&callable)
12
+ @callable = callable
13
+ end
14
+
15
+ def __target_object__
16
+ @__target_object__ ||= callable.call
17
+ end
18
+
19
+ def ==(other)
20
+ __target_object__ == other
21
+ end
22
+
23
+ def !=(other)
24
+ __target_object__ != other
25
+ end
26
+
27
+ def !
28
+ !__target_object__
29
+ end
30
+
31
+ def method_missing(method_name, *args, &block)
32
+ if __target_object__.respond_to?(method_name)
33
+ __target_object__.send(method_name, *args, &block)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(method_name, include_priv = false)
40
+ __target_object__.respond_to?(method_name, include_priv)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class Value
7
+ attr_reader :access_keys
8
+
9
+ def initialize(value, access_keys = [])
10
+ @value = value
11
+ @access_keys = access_keys
12
+ end
13
+
14
+ def evaluate_from(configuration)
15
+ matching_lazy_value = configuration.fetch(@access_keys)
16
+ matching_lazy_value.evaluate
17
+ end
18
+
19
+ def evaluate
20
+ @value
21
+ end
22
+
23
+ def lazy?
24
+ true
25
+ end
26
+
27
+ def reached_by(parent_access_keys, access_key)
28
+ @access_keys = parent_access_keys + [access_key]
29
+ self
30
+ end
31
+
32
+ def to_s
33
+ evaluate.to_s
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueArray < ValueEnumerable
7
+ def initialize(array)
8
+ super
9
+ @value_hash = []
10
+ array.each_with_index do |value, index|
11
+ self[index] = value
12
+ end
13
+ end
14
+
15
+ def evaluate
16
+ @value_hash.map(&:evaluate)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueEnumerable < Value
7
+ def [](key)
8
+ if @value_hash[key].nil?
9
+ Value.new(nil).reached_by(access_keys, key)
10
+ else
11
+ @value_hash[key].reached_by(access_keys, key)
12
+ end
13
+ end
14
+
15
+ def fetch(access_keys)
16
+ fetched_keys = access_keys.dup
17
+ value = self[fetched_keys.shift]
18
+ fetched_keys.any? ? value.fetch(fetched_keys) : value
19
+ end
20
+
21
+ def []=(key, value)
22
+ @value_hash[key] = case value
23
+ when Hash
24
+ ValueHash.new(value)
25
+ when Array
26
+ ValueArray.new(value)
27
+ else
28
+ Value.new(value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Lazy
6
+ class ValueHash < ValueEnumerable
7
+ def initialize(hash)
8
+ super
9
+ @value_hash = ActiveSupport::HashWithIndifferentAccess.new
10
+ hash.each do |key, value|
11
+ self[key] = value
12
+ end
13
+ end
14
+
15
+ def evaluate
16
+ @value_hash.transform_values(&:evaluate)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ class MediaType
6
+ attr_reader :type, :subtype, :vendor, :version, :format
7
+
8
+ # based on the HTTP Accept header with the pattern:
9
+ # application/vnd.:vendor-:version+:format
10
+ VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?<vendor>[a-z0-9.\-_!^]+?)(?:-(?<version>[a-z0-9*.]+))?(?:\+(?<format>[a-z0-9*\-.]+))?\z/.freeze
11
+
12
+ def initialize(type:, subtype:)
13
+ @type = type
14
+ @subtype = subtype
15
+ VENDOR_VERSION_HEADER_REGEX.match(subtype) do |m|
16
+ @vendor = m[:vendor]
17
+ @version = m[:version]
18
+ @format = m[:format]
19
+ end
20
+ end
21
+
22
+ def ==(other)
23
+ eql?(other)
24
+ end
25
+
26
+ def eql?(other)
27
+ self.class == other.class &&
28
+ other.type == type &&
29
+ other.subtype == subtype &&
30
+ other.vendor == vendor &&
31
+ other.version == version &&
32
+ other.format == format
33
+ end
34
+
35
+ def hash
36
+ [self.class, type, subtype, vendor, version, format].hash
37
+ end
38
+
39
+ class << self
40
+ def best_quality(header, available_media_types)
41
+ parse(best_quality_media_type(header, available_media_types))
42
+ end
43
+
44
+ def parse(media_type)
45
+ return if media_type.blank?
46
+
47
+ type, subtype = media_type.split('/', 2)
48
+ return if type.blank? || subtype.blank?
49
+
50
+ new(type: type, subtype: subtype)
51
+ end
52
+
53
+ def match?(media_type)
54
+ return false if media_type.blank?
55
+
56
+ subtype = media_type.split('/', 2).last
57
+ return false if subtype.blank?
58
+
59
+ VENDOR_VERSION_HEADER_REGEX.match?(subtype)
60
+ end
61
+
62
+ def best_quality_media_type(header, available_media_types)
63
+ header.blank? ? available_media_types.first : Rack::Utils.best_q_match(header, available_media_types)
64
+ end
65
+ end
66
+
67
+ private_class_method :best_quality_media_type
68
+ end
69
+ end
70
+ end