Skip to content

Commit 9db8a65

Browse files
committed
Added support for accepting application/json payload in addition to
the existing application/x-www-form-urlencoded. The key for this is that the third element of the FieldStorage is a string as opposed to a list. So the code checks for the string and that the Content-Type is exactly application/json. I do a string match for the Content-Type. This code also adds testing for the dispatch method of RestfulInstance. It tests dispatch using GET, PUT, POST, PATCH methods with json and form data payloads. Existing tests bypass the dispatch method. It moves check for pretty printing till after the input payload is checked to see if it's json. So you can set pretty in the json payload if wanted. Adds a new class: SimulateFieldStorageFromJson. This class emulates the calling interface of FieldStorage. The json payload is parsed into this class. Then the new object is passed off to the code that expects a FieldStorage class. Note that this may or may not work for file uploads, but for issue creation, setting properties, patching objects, it seems to work. Also refactored/replaced the etag header checks to use a more generic method that will work for any header (e.g. Content-Type). Future enhancements are to parse the full form of the Content-Type mime type so something like: application/vnd.roundup.v1+json will also work. Also the SimulateFieldStorageFromJson could be used to represent XML format input, if so need to rename the class dropping FromJson. But because of the issues with native xml parsers in python parsing untrusted data, we may not want to go that route. curl examples for my tracker is: curl -s -u user:pass -X POST --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ --data '{"title": "foo bar", "fyi": "text", "private": "true", "priority": "high" }' \ -w "http status: %{http_code}\n" \ "https://example.net/demo/rest/data/issue" { "data": { "link": "https://example.net/demo/rest/data/issue/2229", "id": "2229" } } http status: 201
1 parent 82049a9 commit 9db8a65

File tree

3 files changed

+282
-20
lines changed

3 files changed

+282
-20
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Features:
6565
collection property. Add @links property with self, next and prev
6666
links (where needed). Add @total_size with size of entire
6767
collection (unpaginated). Pagination index starts at 1 not 0.
68+
- accept content-type application/json payload for PUT, PATCH, POST
69+
requests in addition to application/x-www-form-urlencoded.
6870
(John Rouillard)
6971
- issue2550833: the export_csv web action now returns labels/names
7072
rather than id's. Replace calls to export_csv with the export_csv_id

roundup/rest.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,12 +1331,6 @@ def dispatch(self, method, uri, input):
13311331
# .../issue.json -> .../issue
13321332
uri = uri[:-( len(ext_type) + 1 )]
13331333

1334-
# check for pretty print
1335-
try:
1336-
pretty_output = not input['pretty'].value.lower() == "false"
1337-
except KeyError:
1338-
pretty_output = True
1339-
13401334
# add access-control-allow-* to support CORS
13411335
self.client.setHeader("Access-Control-Allow-Origin", "*")
13421336
self.client.setHeader(
@@ -1352,6 +1346,35 @@ def dispatch(self, method, uri, input):
13521346
"HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
13531347
)
13541348

1349+
# Is there an input.value with format json data?
1350+
# If so turn it into an object that emulates enough
1351+
# of the FieldStorge methods/props to allow a response.
1352+
content_type_header = headers.getheader('Content-Type', None)
1353+
if type(input.value) == str and content_type_header:
1354+
parsed_content_type_header = content_type_header
1355+
# the structure of a content-type header
1356+
# is complex: mime-type; options(charset ...)
1357+
# for now we just accept application/json.
1358+
# FIXME there should be a function:
1359+
# parse_content_type_header(content_type_header)
1360+
# that returns a tuple like the Accept header parser.
1361+
# Then the test below could use:
1362+
# parsed_content_type_header[0].lower() == 'json'
1363+
# That way we could handle stuff like:
1364+
# application/vnd.roundup-foo+json; charset=UTF8
1365+
# for example.
1366+
if content_type_header.lower() == "application/json":
1367+
try:
1368+
input = SimulateFieldStorageFromJson(input.value)
1369+
except ValueError as msg:
1370+
output = self.error_obj(400, msg)
1371+
1372+
# check for pretty print
1373+
try:
1374+
pretty_output = not input['pretty'].value.lower() == "false"
1375+
except KeyError:
1376+
pretty_output = True
1377+
13551378
# Call the appropriate method
13561379
try:
13571380
# If output was defined by a prior error
@@ -1392,3 +1415,48 @@ def default(self, obj):
13921415
except TypeError:
13931416
result = str(obj)
13941417
return result
1418+
1419+
class SimulateFieldStorageFromJson():
1420+
'''
1421+
The internals of the rest interface assume the data was sent as
1422+
application/x-www-form-urlencoded. So we should have a
1423+
FieldStorage and MiniFieldStorage structure.
1424+
1425+
However if we want to handle json data, we need to:
1426+
1) create the Fieldstorage/MiniFieldStorage structure
1427+
or
1428+
2) simultate the interface parts of FieldStorage structure
1429+
1430+
To do 2, create a object that emulates the:
1431+
1432+
object['prop'].value
1433+
1434+
references used when accessing a FieldStorage structure.
1435+
1436+
That's what this class does.
1437+
1438+
'''
1439+
def __init__(self, json_string):
1440+
''' Parse the json string into an internal dict. '''
1441+
def raise_error_on_constant(x):
1442+
raise ValueError, "Unacceptable number: %s"%x
1443+
1444+
self.json_dict = json.loads(json_string,
1445+
parse_constant = raise_error_on_constant)
1446+
self.value = [ self.FsValue(index, self.json_dict[index]) for index in self.json_dict.keys() ]
1447+
1448+
class FsValue:
1449+
'''Class that does nothing but response to a .value property '''
1450+
def __init__(self, name, val):
1451+
self.name=name
1452+
self.value=val
1453+
1454+
def __getitem__(self, index):
1455+
'''Return an FsValue created from the value of self.json_dict[index]
1456+
'''
1457+
return self.FsValue(index, self.json_dict[index])
1458+
1459+
def __contains__(self, index):
1460+
''' implement: 'foo' in DICT '''
1461+
return index in self.json_dict
1462+

0 commit comments

Comments
 (0)