grape 2.0.0 → 2.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -1
- data/README.md +362 -316
- data/UPGRADING.md +197 -7
- data/grape.gemspec +5 -6
- data/lib/grape/api/instance.rb +14 -11
- data/lib/grape/api.rb +19 -10
- data/lib/grape/content_types.rb +0 -2
- data/lib/grape/cookies.rb +2 -1
- data/lib/grape/dry_types.rb +0 -2
- data/lib/grape/dsl/desc.rb +22 -20
- data/lib/grape/dsl/headers.rb +1 -1
- data/lib/grape/dsl/inside_route.rb +42 -13
- data/lib/grape/dsl/parameters.rb +5 -4
- data/lib/grape/dsl/routing.rb +20 -4
- data/lib/grape/dsl/validations.rb +13 -0
- data/lib/grape/endpoint.rb +14 -17
- data/lib/grape/{util/env.rb → env.rb} +0 -5
- data/lib/grape/error_formatter/txt.rb +11 -10
- data/lib/grape/exceptions/base.rb +3 -3
- data/lib/grape/exceptions/validation.rb +0 -2
- data/lib/grape/exceptions/validation_array_errors.rb +1 -0
- data/lib/grape/exceptions/validation_errors.rb +2 -4
- data/lib/grape/extensions/hash.rb +5 -1
- data/lib/grape/http/headers.rb +18 -34
- data/lib/grape/{util/json.rb → json.rb} +1 -3
- data/lib/grape/locale/en.yml +3 -0
- data/lib/grape/middleware/auth/base.rb +0 -2
- data/lib/grape/middleware/auth/dsl.rb +0 -2
- data/lib/grape/middleware/base.rb +1 -3
- data/lib/grape/middleware/error.rb +55 -50
- data/lib/grape/middleware/formatter.rb +16 -13
- data/lib/grape/middleware/globals.rb +1 -3
- data/lib/grape/middleware/stack.rb +4 -5
- data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
- data/lib/grape/middleware/versioner/header.rb +17 -163
- data/lib/grape/middleware/versioner/param.rb +2 -4
- data/lib/grape/middleware/versioner/path.rb +1 -3
- data/lib/grape/namespace.rb +3 -4
- data/lib/grape/path.rb +24 -29
- data/lib/grape/request.rb +4 -12
- data/lib/grape/router/base_route.rb +39 -0
- data/lib/grape/router/greedy_route.rb +20 -0
- data/lib/grape/router/pattern.rb +39 -30
- data/lib/grape/router/route.rb +22 -59
- data/lib/grape/router.rb +32 -37
- data/lib/grape/util/accept_header_handler.rb +105 -0
- data/lib/grape/util/base_inheritable.rb +4 -4
- data/lib/grape/util/cache.rb +0 -3
- data/lib/grape/util/endpoint_configuration.rb +1 -1
- data/lib/grape/util/header.rb +13 -0
- data/lib/grape/util/inheritable_values.rb +0 -2
- data/lib/grape/util/lazy/block.rb +29 -0
- data/lib/grape/util/lazy/object.rb +45 -0
- data/lib/grape/util/lazy/value.rb +38 -0
- data/lib/grape/util/lazy/value_array.rb +21 -0
- data/lib/grape/util/lazy/value_enumerable.rb +34 -0
- data/lib/grape/util/lazy/value_hash.rb +21 -0
- data/lib/grape/util/media_type.rb +70 -0
- data/lib/grape/util/reverse_stackable_values.rb +1 -6
- data/lib/grape/util/stackable_values.rb +1 -6
- data/lib/grape/util/strict_hash_configuration.rb +3 -3
- data/lib/grape/validations/attributes_doc.rb +38 -36
- data/lib/grape/validations/attributes_iterator.rb +1 -0
- data/lib/grape/validations/contract_scope.rb +71 -0
- data/lib/grape/validations/params_scope.rb +15 -18
- data/lib/grape/validations/types/array_coercer.rb +0 -2
- data/lib/grape/validations/types/build_coercer.rb +69 -71
- data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
- data/lib/grape/validations/types/json.rb +0 -2
- data/lib/grape/validations/types/primitive_coercer.rb +0 -2
- data/lib/grape/validations/types/set_coercer.rb +0 -3
- data/lib/grape/validations/types.rb +0 -3
- data/lib/grape/validations/validators/base.rb +1 -0
- data/lib/grape/validations/validators/default_validator.rb +5 -1
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
- data/lib/grape/validations/validators/length_validator.rb +42 -0
- data/lib/grape/validations/validators/values_validator.rb +6 -1
- data/lib/grape/validations.rb +3 -7
- data/lib/grape/version.rb +1 -1
- data/lib/grape/{util/xml.rb → xml.rb} +1 -1
- data/lib/grape.rb +30 -274
- metadata +30 -37
- data/lib/grape/eager_load.rb +0 -20
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
- data/lib/grape/router/attribute_translator.rb +0 -63
- data/lib/grape/util/lazy_block.rb +0 -27
- data/lib/grape/util/lazy_object.rb +0 -43
- data/lib/grape/util/lazy_value.rb +0 -91
data/lib/grape/router/route.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
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.
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
27
|
+
(Grape::Http::Headers::SUPPORTED_METHODS + ['*']).each do |method|
|
28
|
+
next unless map.key?(method)
|
29
|
+
|
35
30
|
routes = map[method]
|
36
|
-
|
37
|
-
|
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
|
-
|
51
|
-
|
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
|
-
|
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 ==
|
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 &&
|
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 !
|
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[
|
130
|
-
method = env[
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
data/lib/grape/util/cache.rb
CHANGED
@@ -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
|
@@ -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
|