Mercurial > p > roundup > code
comparison roundup/rest.py @ 5643:a60cbbcc9309
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
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sun, 10 Mar 2019 17:35:25 -0400 |
| parents | f576957cbb1f |
| children | b4d7588c74a4 |
comparison
equal
deleted
inserted
replaced
| 5642:bd681700e556 | 5643:a60cbbcc9309 |
|---|---|
| 1329 if ( ext_type ): | 1329 if ( ext_type ): |
| 1330 # strip extension so uri make sense | 1330 # strip extension so uri make sense |
| 1331 # .../issue.json -> .../issue | 1331 # .../issue.json -> .../issue |
| 1332 uri = uri[:-( len(ext_type) + 1 )] | 1332 uri = uri[:-( len(ext_type) + 1 )] |
| 1333 | 1333 |
| 1334 # check for pretty print | |
| 1335 try: | |
| 1336 pretty_output = not input['pretty'].value.lower() == "false" | |
| 1337 except KeyError: | |
| 1338 pretty_output = True | |
| 1339 | |
| 1340 # add access-control-allow-* to support CORS | 1334 # add access-control-allow-* to support CORS |
| 1341 self.client.setHeader("Access-Control-Allow-Origin", "*") | 1335 self.client.setHeader("Access-Control-Allow-Origin", "*") |
| 1342 self.client.setHeader( | 1336 self.client.setHeader( |
| 1343 "Access-Control-Allow-Headers", | 1337 "Access-Control-Allow-Headers", |
| 1344 "Content-Type, Authorization, X-HTTP-Method-Override" | 1338 "Content-Type, Authorization, X-HTTP-Method-Override" |
| 1350 self.client.setHeader( | 1344 self.client.setHeader( |
| 1351 "Access-Control-Allow-Methods", | 1345 "Access-Control-Allow-Methods", |
| 1352 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" | 1346 "HEAD, OPTIONS, GET, PUT, DELETE, PATCH" |
| 1353 ) | 1347 ) |
| 1354 | 1348 |
| 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 | |
| 1355 # Call the appropriate method | 1378 # Call the appropriate method |
| 1356 try: | 1379 try: |
| 1357 # If output was defined by a prior error | 1380 # If output was defined by a prior error |
| 1358 # condition skip call | 1381 # condition skip call |
| 1359 if not output: | 1382 if not output: |
| 1390 try: | 1413 try: |
| 1391 result = json.JSONEncoder.default(self, obj) | 1414 result = json.JSONEncoder.default(self, obj) |
| 1392 except TypeError: | 1415 except TypeError: |
| 1393 result = str(obj) | 1416 result = str(obj) |
| 1394 return result | 1417 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 |
