Skip to content

Commit 6a9aac6

Browse files
committed
Added ability to parse HTTP accept header
.. to serve the content type correctly committer: Ralf Schlatterbeck <[email protected]>
1 parent a86f857 commit 6a9aac6

File tree

1 file changed

+81
-11
lines changed

1 file changed

+81
-11
lines changed

roundup/rest.py

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,76 @@ def format_object(self, *args, **kwargs):
6969
return result
7070
return format_object
7171

72+
def parse_accept_header(accept):
73+
"""
74+
Parse the Accept header *accept*, returning a list with 3-tuples of
75+
[(str(media_type), dict(params), float(q_value)),] ordered by q values.
76+
77+
If the accept header includes vendor-specific types like::
78+
application/vnd.yourcompany.yourproduct-v1.1+json
79+
80+
It will actually convert the vendor and version into parameters and
81+
convert the content type into `application/json` so appropriate content
82+
negotiation decisions can be made.
83+
84+
Default `q` for values that are not specified is 1.0
85+
86+
# Based on https://gist.github.com/samuraisam/2714195
87+
# Also, based on a snipped found in this project:
88+
# https://github.com/martinblech/mimerender
89+
"""
90+
result = []
91+
for media_range in accept.split(","):
92+
parts = media_range.split(";")
93+
media_type = parts.pop(0).strip()
94+
media_params = []
95+
# convert vendor-specific content types into something useful (see
96+
# docstring)
97+
typ, subtyp = media_type.split('/')
98+
# check for a + in the sub-type
99+
if '+' in subtyp:
100+
# if it exists, determine if the subtype is a vendor-specific type
101+
vnd, sep, extra = subtyp.partition('+')
102+
if vnd.startswith('vnd'):
103+
# and then... if it ends in something like "-v1.1" parse the
104+
# version out
105+
if '-v' in vnd:
106+
vnd, sep, rest = vnd.rpartition('-v')
107+
if len(rest):
108+
# add the version as a media param
109+
try:
110+
version = media_params.append(('version',
111+
float(rest)))
112+
except ValueError:
113+
version = 1.0 # could not be parsed
114+
# add the vendor code as a media param
115+
media_params.append(('vendor', vnd))
116+
# and re-write media_type to something like application/json so
117+
# it can be used usefully when looking up emitters
118+
media_type = '{}/{}'.format(typ, extra)
119+
q = 1.0
120+
for part in parts:
121+
(key, value) = part.lstrip().split("=", 1)
122+
key = key.strip()
123+
value = value.strip()
124+
if key == "q":
125+
q = float(value)
126+
else:
127+
media_params.append((key, value))
128+
result.append((media_type, dict(media_params), q))
129+
result.sort(lambda x, y: -cmp(x[2], y[2]))
130+
return result
72131

73132
class RestfulInstance(object):
74133
"""The RestfulInstance performs REST request from the client"""
75134

76135
__default_patch_op = "replace" # default operator for PATCH method
136+
__accepted_content_type = {
137+
"application/json": "json",
138+
"*/*": "json"
139+
# "application/xml": "xml"
140+
}
141+
__default_accept_type = "json"
77142

78143
def __init__(self, client, db):
79144
self.client = client
@@ -778,26 +843,23 @@ def option_attribute(self, class_name, item_id, attr_name, input):
778843

779844
def dispatch(self, method, uri, input):
780845
"""format and process the request"""
781-
# PATH is split to multiple pieces
782-
# 0 - rest
783-
# 1 - resource
784-
# 2 - attribute
785-
uri_split = uri.split("/")
786-
resource_uri = uri_split[1]
787-
788846
# if X-HTTP-Method-Override is set, follow the override method
789847
headers = self.client.request.headers
790848
method = headers.getheader('X-HTTP-Method-Override') or method
791849

850+
# parse Accept header and get the content type
851+
accept_header = parse_accept_header(headers.getheader('Accept'))
852+
accept_type = "invalid"
853+
for part in accept_header:
854+
if part[0] in self.__accepted_content_type:
855+
accept_type = self.__accepted_content_type[part[0]]
856+
792857
# get the request format for response
793858
# priority : extension from uri (/rest/issue.json),
794859
# header (Accept: application/json, application/xml)
795860
# default (application/json)
796-
797-
# format_header need a priority parser
798861
ext_type = os.path.splitext(urlparse.urlparse(uri).path)[1][1:]
799-
accept_header = headers.getheader('Accept')[12:]
800-
data_type = ext_type or accept_header or "json"
862+
data_type = ext_type or accept_type or self.__default_accept_type
801863

802864
# check for pretty print
803865
try:
@@ -819,6 +881,14 @@ def dispatch(self, method, uri, input):
819881
"Access-Control-Allow-Methods",
820882
"HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
821883
)
884+
885+
# PATH is split to multiple pieces
886+
# 0 - rest
887+
# 1 - resource
888+
# 2 - attribute
889+
uri_split = uri.split("/")
890+
resource_uri = uri_split[1]
891+
822892
try:
823893
class_name, item_id = hyperdb.splitDesignator(resource_uri)
824894
except hyperdb.DesignatorError:

0 commit comments

Comments
 (0)