Commit 5312ece5 authored by Mark Moissette's avatar Mark Moissette

- added support for OPTIONS, HEAD , and PATCH http verbs

- added support for json serialization using jsonpickle 

- moved convertToJson to convert module

- removed forced content type setting in __generateResponse that
sets MediaType.TEXT_PLAIN as content type, when response is a string :
it should be set exclusively based on what the caller can accept/ has
asked for, as specified in the docstring for this method

- fixed bug that would mangle routed urls after passing more than once
in __registerRouters : this is visible when running unit tests or any
other situation when a RestResource is recreated with the same
"service"
parent 1d843ea2
...@@ -7,12 +7,22 @@ for JSON/XML/YAML output ...@@ -7,12 +7,22 @@ for JSON/XML/YAML output
''' '''
import collections import collections
import logging
from UserDict import DictMixin from UserDict import DictMixin
from twisted.python import log
advanced_json = False
try:
import jsonpickle
advanced_json = True
except ImportError:
import json
primitives = (int, long, float, bool, str,unicode) primitives = (int, long, float, bool, str,unicode)
def convertForSerialization(obj): def convertForSerialization(obj):
"""Converts anything (clas,tuples,list) to the safe serializable equivalent""" """Converts anything (clas,tuples,list) to the safe serializable equivalent"""
try:
if type(obj) in primitives: if type(obj) in primitives:
# no conversion # no conversion
return obj return obj
...@@ -29,6 +39,9 @@ def convertForSerialization(obj): ...@@ -29,6 +39,9 @@ def convertForSerialization(obj):
else: else:
# return as-is # return as-is
return obj return obj
except AttributeError as ex:
log.msg(ex,logLevel=logging.WARN)
return obj
def convertClassToDict(clazz): def convertClassToDict(clazz):
"""Converts a class to a dictionary""" """Converts a class to a dictionary"""
...@@ -49,6 +62,19 @@ def traverseDict(dictObject): ...@@ -49,6 +62,19 @@ def traverseDict(dictObject):
return newDict return newDict
def convertToJson(obj):
"""Converts to JSON, including Python classes that are not JSON serializable by default"""
if advanced_json:
try:
return jsonpickle.encode(obj, unpicklable=False)
except Exception as ex:
raise RuntimeError(str(ex))
try:
return json.dumps(obj)
except Exception as ex:
raise RuntimeError(str(ex))
def generateXml(obj): def generateXml(obj):
"""Generates basic XML from an object that has already been converted for serialization""" """Generates basic XML from an object that has already been converted for serialization"""
if isinstance(obj, dict) or isinstance(obj,DictMixin): if isinstance(obj, dict) or isinstance(obj,DictMixin):
......
...@@ -4,18 +4,24 @@ Common enums ...@@ -4,18 +4,24 @@ Common enums
@author: jacekf @author: jacekf
''' '''
class Http: class Http:
"""Enumerates HTTP methods""" """Enumerates HTTP methods"""
GET = "GET" GET = "GET"
POST = "POST" POST = "POST"
PUT = "PUT" PUT = "PUT"
DELETE = "DELETE" DELETE = "DELETE"
OPTIONS = "OPTIONS"
HEAD = "HEAD"
PATCH = "PATCH"
class HttpHeader: class HttpHeader:
"""Enumerates common HTTP headers""" """Enumerates common HTTP headers"""
CONTENT_TYPE = "content-type" CONTENT_TYPE = "content-type"
ACCEPT = "accept" ACCEPT = "accept"
class MediaType: class MediaType:
"""Enumerates media types""" """Enumerates media types"""
WILDCARD = "*/*" WILDCARD = "*/*"
......
...@@ -7,17 +7,25 @@ Common routing classes, regardless of whether used in HTTP or multiprocess conte ...@@ -7,17 +7,25 @@ Common routing classes, regardless of whether used in HTTP or multiprocess conte
from collections import defaultdict from collections import defaultdict
from corepost import Response, RESTException from corepost import Response, RESTException
from corepost.enums import Http, HttpHeader from corepost.enums import Http, HttpHeader
from corepost.utils import getMandatoryArgumentNames, convertToJson, safeDictUpdate from corepost.utils import getMandatoryArgumentNames, safeDictUpdate
from corepost.convert import convertForSerialization, generateXml from corepost.convert import convertForSerialization, generateXml, convertToJson
from corepost.filters import IRequestFilter, IResponseFilter from corepost.filters import IRequestFilter, IResponseFilter
from enums import MediaType from enums import MediaType
from twisted.internet import defer from twisted.internet import defer
from twisted.web.http import parse_qs from twisted.web.http import parse_qs
from twisted.python import log from twisted.python import log
import re, copy, exceptions, json, yaml, logging import re, copy, exceptions, yaml, logging
from xml.etree import ElementTree from xml.etree import ElementTree
advanced_json = False
try:
import jsonpickle
advanced_json = True
except ImportError:
import json
class UrlRouter: class UrlRouter:
''' Common class for containing info related to routing a request to a function ''' ''' Common class for containing info related to routing a request to a function '''
...@@ -140,8 +148,8 @@ class RequestRouter: ...@@ -140,8 +148,8 @@ class RequestRouter:
''' '''
Constructor Constructor
''' '''
self.__urls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict)} self.__urls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict),Http.OPTIONS: defaultdict(dict),Http.PATCH: defaultdict(dict),Http.HEAD: defaultdict(dict)}
self.__cachedUrls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict)} self.__cachedUrls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict),Http.OPTIONS: defaultdict(dict),Http.PATCH: defaultdict(dict),Http.HEAD: defaultdict(dict)}
self.__urlRouterInstances = {} self.__urlRouterInstances = {}
self.__schema = schema self.__schema = schema
self.__registerRouters(restServiceContainer) self.__registerRouters(restServiceContainer)
...@@ -167,7 +175,7 @@ class RequestRouter: ...@@ -167,7 +175,7 @@ class RequestRouter:
def path(self): def path(self):
return self.__path return self.__path
def __registerRouters(self,restServiceContainer): def __registerRouters(self, restServiceContainer):
"""Main method responsible for registering routers""" """Main method responsible for registering routers"""
from types import FunctionType from types import FunctionType
...@@ -182,15 +190,17 @@ class RequestRouter: ...@@ -182,15 +190,17 @@ class RequestRouter:
# if specified, add class path to each function's path # if specified, add class path to each function's path
rq = func.corepostRequestRouter rq = func.corepostRequestRouter
#workaround for multiple passes of __registerRouters (for unit tests etc)
if not hasattr(rq, 'urlAdapted'):
rq.url = "%s%s" % (rootPath,rq.url) rq.url = "%s%s" % (rootPath,rq.url)
# remove first and trailing '/' to standardize URLs # remove first and trailing '/' to standardize URLs
start = 1 if rq.url[0:1] == "/" else 0 start = 1 if rq.url[0:1] == "/" else 0
end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url) end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url)
rq.url = rq.url[start:end] rq.url = rq.url[start:end]
setattr(rq,'urlAdapted',True)
# now that the full URL is set, compile the matcher for it # now that the full URL is set, compile the matcher for it
rq.compileMatcherForFullUrl() rq.compileMatcherForFullUrl()
for method in rq.methods: for method in rq.methods:
for accepts in rq.accepts: for accepts in rq.accepts:
urlRouterInstance = UrlRouterInstance(service,rq) urlRouterInstance = UrlRouterInstance(service,rq)
...@@ -286,6 +296,9 @@ class RequestRouter: ...@@ -286,6 +296,9 @@ class RequestRouter:
log.err(ex) log.err(ex)
response = self.__createErrorResponse(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex)) response = self.__createErrorResponse(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex))
#if a url is defined, but not the requested method
elif self.__urls[request.method].get(path, {}) == {}:
response = self.__createErrorResponse(request,501, "")
else: else:
log.msg("URL %s not found" % path,logLevel=logging.WARN) log.msg("URL %s not found" % path,logLevel=logging.WARN)
response = self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path) response = self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path)
...@@ -305,9 +318,7 @@ class RequestRouter: ...@@ -305,9 +318,7 @@ class RequestRouter:
Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML) Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML)
depending on what the caller can accept. Returns Response depending on what the caller can accept. Returns Response
""" """
if isinstance(response, str): if isinstance(response, Response):
return Response(code,response,{HttpHeader.CONTENT_TYPE: MediaType.TEXT_PLAIN})
elif isinstance(response, Response):
return response return response
else: else:
(content,contentType) = self.__convertObjectToContentType(request, response) (content,contentType) = self.__convertObjectToContentType(request, response)
...@@ -318,20 +329,27 @@ class RequestRouter: ...@@ -318,20 +329,27 @@ class RequestRouter:
Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept. Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept.
Returns a tuple of (content,contentType) Returns a tuple of (content,contentType)
""" """
obj = convertForSerialization(obj)
if HttpHeader.ACCEPT in request.received_headers: if HttpHeader.ACCEPT in request.received_headers:
accept = request.received_headers[HttpHeader.ACCEPT] accept = request.received_headers[HttpHeader.ACCEPT]
if MediaType.APPLICATION_JSON in accept: if MediaType.APPLICATION_JSON in accept:
if not advanced_json:
obj = convertForSerialization(obj)
return (convertToJson(obj),MediaType.APPLICATION_JSON) return (convertToJson(obj),MediaType.APPLICATION_JSON)
elif MediaType.TEXT_YAML in accept: elif MediaType.TEXT_YAML in accept:
obj = convertForSerialization(obj)
return (yaml.dump(obj),MediaType.TEXT_YAML) return (yaml.dump(obj),MediaType.TEXT_YAML)
elif MediaType.APPLICATION_XML in accept or MediaType.TEXT_XML in accept: elif MediaType.APPLICATION_XML in accept or MediaType.TEXT_XML in accept:
obj = convertForSerialization(obj)
return (generateXml(obj),MediaType.APPLICATION_XML) return (generateXml(obj),MediaType.APPLICATION_XML)
else: else:
# no idea, let's do JSON # no idea, let's do JSON
if not advanced_json:
obj = convertForSerialization(obj)
return (convertToJson(obj),MediaType.APPLICATION_JSON) return (convertToJson(obj),MediaType.APPLICATION_JSON)
else: else:
if not advanced_json:
obj = convertForSerialization(obj)
# called has no accept header, let's default to JSON # called has no accept header, let's default to JSON
return (convertToJson(obj),MediaType.APPLICATION_JSON) return (convertToJson(obj),MediaType.APPLICATION_JSON)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Various CorePost utilities Various CorePost utilities
''' '''
from inspect import getargspec from inspect import getargspec
import json
def getMandatoryArgumentNames(f): def getMandatoryArgumentNames(f):
'''Returns a tuple of the mandatory arguments required in a function''' '''Returns a tuple of the mandatory arguments required in a function'''
...@@ -12,16 +12,11 @@ def getMandatoryArgumentNames(f): ...@@ -12,16 +12,11 @@ def getMandatoryArgumentNames(f):
else: else:
return args[0:len(args) - len(defaults)] return args[0:len(args) - len(defaults)]
def getRouterKey(method,url): def getRouterKey(method,url):
'''Returns the common key used to represent a function that a request can be routed to''' '''Returns the common key used to represent a function that a request can be routed to'''
return "%s %s" % (method,url) return "%s %s" % (method,url)
def convertToJson(obj):
"""Converts to JSON, including Python classes that are not JSON serializable by default"""
try:
return json.dumps(obj)
except Exception as ex:
raise RuntimeError(str(ex))
def checkExpectedInterfaces(objects,expectedInterface): def checkExpectedInterfaces(objects,expectedInterface):
"""Verifies that all the objects implement the expected interface""" """Verifies that all the objects implement the expected interface"""
...@@ -33,4 +28,3 @@ def safeDictUpdate(dictObject,key,value): ...@@ -33,4 +28,3 @@ def safeDictUpdate(dictObject,key,value):
"""Only adds a key to a dictionary. If key exists, it leaves it untouched""" """Only adds a key to a dictionary. If key exists, it leaves it untouched"""
if key not in dictObject: if key not in dictObject:
dictObject[key] = value dictObject[key] = value
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment