@@ -22,54 +22,78 @@ module Versioner
22
22
# X-Cascade header to alert Rack::Mount to attempt the next matched
23
23
# route.
24
24
class Header < Base
25
+ VENDOR_VERSION_HEADER_REGEX =
26
+ /\A vnd\. ([a-z0-9*.]+)(?:-([a-z0-9*\- .]+))?(?:\+ ([a-z0-9*\- .+]+))?\z /
27
+
25
28
def before
26
- header = rack_accept_header
29
+ strict_header_checks if strict?
27
30
28
- if strict?
29
- # If no Accept header:
30
- if header . qvalues . empty?
31
- fail Grape ::Exceptions ::InvalidAcceptHeader . new ( 'Accept header must be set.' , error_headers )
32
- end
33
- # Remove any acceptable content types with ranges.
34
- header . qvalues . reject! do |media_type , _ |
35
- Rack ::Accept ::Header . parse_media_type ( media_type ) . find { |s | s == '*' }
36
- end
37
- # If all Accept headers included a range:
38
- if header . qvalues . empty?
39
- fail Grape ::Exceptions ::InvalidAcceptHeader . new ( 'Accept header must not contain ranges ("*").' ,
40
- error_headers )
41
- end
31
+ if media_type
32
+ media_type_header_handler
33
+ elsif headers_contain_wrong_vendor_or_version?
34
+ fail_with_invalid_accept_header! ( 'API vendor or version not found.' )
42
35
end
36
+ end
43
37
44
- media_type = header . best_of available_media_types
38
+ private
45
39
46
- if media_type
47
- type , subtype = Rack ::Accept ::Header . parse_media_type media_type
48
- env [ 'api.type' ] = type
49
- env [ 'api.subtype' ] = subtype
50
-
51
- if /\A vnd\. ([a-z0-9*.]+)(?:-([a-z0-9*\- .]+))?(?:\+ ([a-z0-9*\- .+]+))?\z / =~ subtype
52
- env [ 'api.vendor' ] = Regexp . last_match [ 1 ]
53
- env [ 'api.version' ] = Regexp . last_match [ 2 ]
54
- env [ 'api.format' ] = Regexp . last_match [ 3 ] # weird that Grape::Middleware::Formatter also does this
55
- end
56
- # If none of the available content types are acceptable:
57
- elsif strict?
58
- fail Grape ::Exceptions ::InvalidAcceptHeader . new ( '406 Not Acceptable' , error_headers )
59
- # If all acceptable content types specify a vendor or version that doesn't exist:
60
- elsif header . values . all? { |header_value | has_vendor? ( header_value ) || version? ( header_value ) }
61
- fail Grape ::Exceptions ::InvalidAcceptHeader . new ( 'API vendor or version not found.' , error_headers )
40
+ def strict_header_checks
41
+ strict_accept_header_presence_check
42
+ strict_verion_vendor_accept_header_presence_check
43
+ end
44
+
45
+ def strict_accept_header_presence_check
46
+ return unless header . qvalues . empty?
47
+ fail_with_invalid_accept_header! ( 'Accept header must be set.' )
48
+ end
49
+
50
+ def strict_verion_vendor_accept_header_presence_check
51
+ return unless versions . present?
52
+ return if an_accept_header_with_version_and_vendor_is_present?
53
+ fail_with_invalid_accept_header! ( 'API vendor or version not found.' )
54
+ end
55
+
56
+ def an_accept_header_with_version_and_vendor_is_present?
57
+ header . qvalues . keys . any? do |h |
58
+ VENDOR_VERSION_HEADER_REGEX =~ h . sub ( 'application/' , '' )
62
59
end
63
60
end
64
61
65
- private
62
+ def header
63
+ @header ||= rack_accept_header
64
+ end
65
+
66
+ def media_type
67
+ @media_type ||= header . best_of ( available_media_types )
68
+ end
69
+
70
+ def media_type_header_handler
71
+ type , subtype = Rack ::Accept ::Header . parse_media_type ( media_type )
72
+ env [ 'api.type' ] = type
73
+ env [ 'api.subtype' ] = subtype
74
+
75
+ if VENDOR_VERSION_HEADER_REGEX =~ subtype
76
+ env [ 'api.vendor' ] = Regexp . last_match [ 1 ]
77
+ env [ 'api.version' ] = Regexp . last_match [ 2 ]
78
+ # weird that Grape::Middleware::Formatter also does this
79
+ env [ 'api.format' ] = Regexp . last_match [ 3 ]
80
+ end
81
+ end
82
+
83
+ def fail_with_invalid_accept_header! ( message )
84
+ fail Grape ::Exceptions ::InvalidAcceptHeader
85
+ . new ( message , error_headers )
86
+ end
66
87
67
88
def available_media_types
68
89
available_media_types = [ ]
69
90
70
91
content_types . each do |extension , _media_type |
71
92
versions . reverse_each do |version |
72
- available_media_types += [ "application/vnd.#{ vendor } -#{ version } +#{ extension } " , "application/vnd.#{ vendor } -#{ version } " ]
93
+ available_media_types += [
94
+ "application/vnd.#{ vendor } -#{ version } +#{ extension } " ,
95
+ "application/vnd.#{ vendor } -#{ version } "
96
+ ]
73
97
end
74
98
available_media_types << "application/vnd.#{ vendor } +#{ extension } "
75
99
end
@@ -83,30 +107,42 @@ def available_media_types
83
107
available_media_types . flatten
84
108
end
85
109
110
+ def headers_contain_wrong_vendor_or_version?
111
+ header . values . all? do |header_value |
112
+ has_vendor? ( header_value ) || version? ( header_value )
113
+ end
114
+ end
115
+
86
116
def rack_accept_header
87
117
Rack ::Accept ::MediaType . new env [ Grape ::Http ::Headers ::HTTP_ACCEPT ]
88
118
rescue RuntimeError => e
89
- raise Grape :: Exceptions :: InvalidAcceptHeader . new ( e . message , error_headers )
119
+ fail_with_invalid_accept_header! ( e . message )
90
120
end
91
121
92
122
def versions
93
123
options [ :versions ] || [ ]
94
124
end
95
125
96
126
def vendor
97
- options [ : version_options] && options [ : version_options] [ :vendor ]
127
+ version_options && version_options [ :vendor ]
98
128
end
99
129
100
130
def strict?
101
- options [ :version_options ] && options [ :version_options ] [ :strict ]
131
+ version_options && version_options [ :strict ]
132
+ end
133
+
134
+ def version_options
135
+ options [ :version_options ]
102
136
end
103
137
104
- # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
105
- # of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
106
- # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
138
+ # By default those errors contain an `X-Cascade` header set to `pass`,
139
+ # which allows nesting and stacking of routes
140
+ # (see [Rack::Mount](https://github.com/josh/rack-mount) for more
141
+ # information). To prevent # this behavior, and not add the `X-Cascade`
142
+ # header, one can set the `:cascade` option to `false`.
107
143
def cascade?
108
- if options [ : version_options] && options [ : version_options] . key? ( :cascade )
109
- !!options [ : version_options] [ :cascade ]
144
+ if version_options && version_options . key? ( :cascade )
145
+ !!version_options [ :cascade ]
110
146
else
111
147
true
112
148
end
@@ -119,14 +155,14 @@ def error_headers
119
155
# @param [String] media_type a content type
120
156
# @return [Boolean] whether the content type sets a vendor
121
157
def has_vendor? ( media_type )
122
- _ , subtype = Rack ::Accept ::Header . parse_media_type media_type
158
+ _ , subtype = Rack ::Accept ::Header . parse_media_type ( media_type )
123
159
subtype [ /\A vnd\. [a-z0-9*.]+/ ]
124
160
end
125
161
126
162
# @param [String] media_type a content type
127
163
# @return [Boolean] whether the content type sets an API version
128
164
def version? ( media_type )
129
- _ , subtype = Rack ::Accept ::Header . parse_media_type media_type
165
+ _ , subtype = Rack ::Accept ::Header . parse_media_type ( media_type )
130
166
subtype [ /\A vnd\. [a-z0-9*.]+-[a-z0-9*\- .]+/ ]
131
167
end
132
168
end
0 commit comments