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,27 +7,40 @@ for JSON/XML/YAML output ...@@ -7,27 +7,40 @@ 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"""
if type(obj) in primitives: try:
if type(obj) in primitives:
# no conversion # no conversion
return obj return obj
elif isinstance(obj, dict) or isinstance(obj,DictMixin): elif isinstance(obj, dict) or isinstance(obj,DictMixin):
return traverseDict(obj) return traverseDict(obj)
elif isClassInstance(obj): elif isClassInstance(obj):
return convertClassToDict(obj) return convertClassToDict(obj)
elif isinstance(obj,collections.Iterable) and not isinstance(obj,str): elif isinstance(obj,collections.Iterable) and not isinstance(obj,str):
# iterable # iterable
values = [] values = []
for val in obj: for val in obj:
values.append(convertForSerialization(val)) values.append(convertForSerialization(val))
return values return values
else: else:
# return as-is # return as-is
return obj
except AttributeError as ex:
log.msg(ex,logLevel=logging.WARN)
return obj return obj
def convertClassToDict(clazz): def convertClassToDict(clazz):
...@@ -48,6 +61,19 @@ def traverseDict(dictObject): ...@@ -48,6 +61,19 @@ def traverseDict(dictObject):
newDict[prop] = convertForSerialization(val) newDict[prop] = convertForSerialization(val)
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"""
......
...@@ -4,20 +4,26 @@ Common enums ...@@ -4,20 +4,26 @@ 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 = "*/*"
APPLICATION_XML = "application/xml" APPLICATION_XML = "application/xml"
APPLICATION_ATOM_XML = "application/atom+xml" APPLICATION_ATOM_XML = "application/atom+xml"
......
...@@ -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
rq.url = "%s%s" % (rootPath,rq.url) #workaround for multiple passes of __registerRouters (for unit tests etc)
# remove first and trailing '/' to standardize URLs if not hasattr(rq, 'urlAdapted'):
start = 1 if rq.url[0:1] == "/" else 0 rq.url = "%s%s" % (rootPath,rq.url)
end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url) # remove first and trailing '/' to standardize URLs
rq.url = rq.url[start:end] start = 1 if rq.url[0:1] == "/" else 0
end = -1 if rq.url[len(rq.url) -1] == '/' else len(rq.url)
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)
...@@ -285,7 +295,10 @@ class RequestRouter: ...@@ -285,7 +295,10 @@ class RequestRouter:
except Exception as ex: except Exception as ex:
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)
...@@ -352,7 +370,7 @@ class RequestRouter: ...@@ -352,7 +370,7 @@ class RequestRouter:
"""Finishes any Defered/inlineCallback methods that raised an error. Returns Response""" """Finishes any Defered/inlineCallback methods that raised an error. Returns Response"""
log.err(error, "Deferred failed") log.err(error, "Deferred failed")
return self.__createErrorResponse(request, 500,"Internal server error") return self.__createErrorResponse(request, 500,"Internal server error")
def __createErrorResponse(self,request,code,message): def __createErrorResponse(self,request,code,message):
"""Common method for rendering errors""" """Common method for rendering errors"""
return Response(code=code, entity=message, headers={"content-type": MediaType.TEXT_PLAIN}) return Response(code=code, entity=message, headers={"content-type": MediaType.TEXT_PLAIN})
......
...@@ -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'''
...@@ -11,17 +11,12 @@ def getMandatoryArgumentNames(f): ...@@ -11,17 +11,12 @@ def getMandatoryArgumentNames(f):
return args return args
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