Commit 80435508 authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

full working example with a complete REST service, new REST exception framework, etc.

parent 89864450
...@@ -10,7 +10,7 @@ from zope.interface import Interface, Attribute ...@@ -10,7 +10,7 @@ from zope.interface import Interface, Attribute
# #
######################################################### #########################################################
class IRestServiceContainer(Interface): class IRESTResource(Interface):
"""An interface for all REST services that can be added within a root CorePost resource""" """An interface for all REST services that can be added within a root CorePost resource"""
services = Attribute("All the REST services contained in this resource") services = Attribute("All the REST services contained in this resource")
...@@ -27,8 +27,28 @@ class Response: ...@@ -27,8 +27,28 @@ class Response:
""" """
def __init__(self,code=200,entity=None,headers={}): def __init__(self,code=200,entity=None,headers={}):
self.code = code self.code = code
self.entity=entity self.entity=entity if entity != None else ""
self.headers=headers self.headers=headers
def __str__(self): def __str__(self):
return str(self.__dict__) return str(self.__dict__)
class RESTException(Exception):
"""Standard REST exception that gets converted to the Response it passes in"""
def __init__(self, response):
self.response = response
class NotFoundException(RESTException):
"""Standard 404 exception when REST resource is not found"""
def __init__(self, resourceName, invalidValue):
RESTException.__init__(self,Response(404,"Unable to find %s identified by '%s'" % (resourceName,invalidValue), {"x-corepost-resource":resourceName,"x-corepost-value":invalidValue}))
class ConflictException(RESTException):
"""Standard 409 exception when REST resource is not found. Allows to pass in a custom message with more details"""
def __init__(self, resourceName, invalidValue, message):
RESTException.__init__(self,Response(409,"Conflict for %s identified by '%s': %s" % (resourceName,invalidValue, message), {"x-corepost-resource":resourceName,"x-corepost-value":invalidValue}))
class AlreadyExistsException(ConflictException):
"""Standard 409 exception when REST resource already exists during a POST"""
def __init__(self, resourceName, invalidValue, message):
ConflictException.__init__(self, resourceName, invalidValue, "%s already exists" % resourceName)
...@@ -5,7 +5,7 @@ Created on 2011-10-03 ...@@ -5,7 +5,7 @@ Created on 2011-10-03
Common routing classes, regardless of whether used in HTTP or multiprocess context Common routing classes, regardless of whether used in HTTP or multiprocess context
''' '''
from collections import defaultdict from collections import defaultdict
from corepost import Response from corepost import Response, RESTException
from corepost.enums import Http, HttpHeader from corepost.enums import Http, HttpHeader
from corepost.utils import getMandatoryArgumentNames, convertToJson from corepost.utils import getMandatoryArgumentNames, convertToJson
from corepost.convert import convertForSerialization, generateXml from corepost.convert import convertForSerialization, generateXml
...@@ -16,7 +16,8 @@ from twisted.internet import reactor, defer ...@@ -16,7 +16,8 @@ from twisted.internet import reactor, defer
from twisted.web.http import parse_qs from twisted.web.http import parse_qs
from twisted.web.resource import Resource from twisted.web.resource import Resource
from twisted.web.server import Site, NOT_DONE_YET from twisted.web.server import Site, NOT_DONE_YET
import re, copy, exceptions, json, yaml from twisted.python import log
import re, copy, exceptions, json, yaml, logging
from xml.etree import ElementTree from xml.etree import ElementTree
from xml.etree.ElementTree import Element from xml.etree.ElementTree import Element
from zope.interface.verify import verifyObject from zope.interface.verify import verifyObject
...@@ -264,6 +265,7 @@ class RequestRouter: ...@@ -264,6 +265,7 @@ class RequestRouter:
urlRouter = urlRouterInstance.urlRouter urlRouter = urlRouterInstance.urlRouter
val = urlRouter.call(urlRouterInstance.clazz,request,**allargs) val = urlRouter.call(urlRouterInstance.clazz,request,**allargs)
#handle Deferreds natively #handle Deferreds natively
if isinstance(val,defer.Deferred): if isinstance(val,defer.Deferred):
# add callback to finish the request # add callback to finish the request
...@@ -281,14 +283,27 @@ class RequestRouter: ...@@ -281,14 +283,27 @@ class RequestRouter:
response = self.__generateResponse(request, val, request.code) response = self.__generateResponse(request, val, request.code)
except exceptions.TypeError as ex: except exceptions.TypeError as ex:
log.err(ex)
response = self.__createErrorResponse(request,400,"%s" % ex) response = self.__createErrorResponse(request,400,"%s" % ex)
except RESTException as ex:
"""Convert REST exceptions to their responses. Input errors log at a lower level to avoid overloading logs"""
if (ex.response.code in (400,404)):
log.msg(ex,logLevel=logging.WARN)
else:
log.err(ex)
response = ex.response
except Exception as ex: except Exception as 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))
else: else:
log.msg(ex,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)
except Exception as ex: except Exception as ex:
log.err(ex)
response = self.__createErrorResponse(request,500,"Internal server error: %s" % ex) response = self.__createErrorResponse(request,500,"Internal server error: %s" % ex)
# response handling # response handling
......
...@@ -3,7 +3,7 @@ Argument extraction tests ...@@ -3,7 +3,7 @@ Argument extraction tests
@author: jacekf @author: jacekf
''' '''
from corepost.web import RestServiceContainer, validate, route from corepost.web import RESTResource, validate, route
from corepost.enums import Http from corepost.enums import Http
from formencode import Schema, validators from formencode import Schema, validators
...@@ -29,5 +29,5 @@ class ArgumentApp(): ...@@ -29,5 +29,5 @@ class ArgumentApp():
return "%s - %s - %s" % (rootId,childId,kwargs) return "%s - %s - %s" % (rootId,childId,kwargs)
def run_app_arguments(): def run_app_arguments():
app = RestServiceContainer((ArgumentApp(),)) app = RESTResource((ArgumentApp(),))
app.run(8082) app.run(8082)
\ No newline at end of file
Using step definitions from: '../steps'
@rest
Feature: REST App
CorePost should be able to build REST applications
for nested REST resources
Background:
Given 'rest_resource' is running
# add a few default customers
When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=d1&firstName=John&lastName=Doe1'
Then I expect HTTP code 201
When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=d2&firstName=John&lastName=Doe2'
Then I expect HTTP code 201
When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=d3&firstName=John&lastName=Doe3'
Then I expect HTTP code 201
@full
Scenario: Full Customer lifecycle
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer'
Then I expect HTTP code 200
And I expect JSON content
"""
[
{
"addresses": [],
"customerId": "d2",
"firstName": "John",
"lastName": "Doe2"
},
{
"addresses": [],
"customerId": "d3",
"firstName": "John",
"lastName": "Doe3"
},
{
"addresses": [],
"customerId": "d1",
"firstName": "John",
"lastName": "Doe1"
}
]
"""
# add 1
When as user 'None:None' I POST 'http://127.0.0.1:8085/customer' with 'customerId=c1&firstName=John&lastName=Doe'
Then I expect HTTP code 201
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/c1'
Then I expect HTTP code 200
And I expect JSON content
"""
{
"addresses": [],
"customerId": "c1",
"firstName": "John",
"lastName": "Doe"
}
"""
# update
When as user 'None:None' I PUT 'http://127.0.0.1:8085/customer/c1' with 'firstName=Jill&lastName=Jones'
Then I expect HTTP code 200
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/c1'
Then I expect HTTP code 200
And I expect JSON content
"""
{
"addresses": [],
"customerId": "c1",
"firstName": "Jill",
"lastName": "Jones"
}
"""
# delete
When as user 'None:None' I DELETE 'http://127.0.0.1:8085/customer/c1'
Then I expect HTTP code 200
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer/c1'
Then I expect HTTP code 404
# delete all
When as user 'None:None' I DELETE 'http://127.0.0.1:8085/customer'
Then I expect HTTP code 200
When as user 'None:None' I GET 'http://127.0.0.1:8085/customer'
Then I expect HTTP code 200
And I expect JSON content
"""
[]
"""
\ No newline at end of file
...@@ -3,7 +3,7 @@ Server tests ...@@ -3,7 +3,7 @@ Server tests
@author: jacekf @author: jacekf
''' '''
from corepost.web import RestServiceContainer, route from corepost.web import RESTResource, route
from corepost.enums import Http from corepost.enums import Http
from corepost.filters import IRequestFilter, IResponseFilter from corepost.filters import IRequestFilter, IResponseFilter
from zope.interface import implements from zope.interface import implements
...@@ -41,7 +41,7 @@ class FilterService(): ...@@ -41,7 +41,7 @@ class FilterService():
return request.received_headers return request.received_headers
def run_filter_app(): def run_filter_app():
app = RestServiceContainer(services=(FilterService(),),filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),)) app = RESTResource(services=(FilterService(),),filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app.run(8083) app.run(8083)
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -3,7 +3,7 @@ Server tests ...@@ -3,7 +3,7 @@ Server tests
@author: jacekf @author: jacekf
''' '''
from corepost.web import RestServiceContainer, route from corepost.web import RESTResource, route
from corepost.enums import Http, MediaType, HttpHeader from corepost.enums import Http, MediaType, HttpHeader
from twisted.internet import defer from twisted.internet import defer
from xml.etree import ElementTree from xml.etree import ElementTree
...@@ -115,7 +115,7 @@ class HomeApp(): ...@@ -115,7 +115,7 @@ class HomeApp():
return self.issue1 return self.issue1
def run_app_home(): def run_app_home():
app = RestServiceContainer((HomeApp(),)) app = RESTResource((HomeApp(),))
app.run() app.run()
if __name__ == "__main__": if __name__ == "__main__":
......
''' '''
A RestServiceContainer module1 that can be merged into the main RestServiceContainer Resource A RESTResource module1 that can be merged into the main RESTResource Resource
''' '''
from corepost.web import RestServiceContainer, route from corepost.web import RESTResource, route
from corepost.enums import Http from corepost.enums import Http
class HomeApp(): class HomeApp():
...@@ -34,7 +34,7 @@ class Module2(): ...@@ -34,7 +34,7 @@ class Module2():
return request.path return request.path
def run_app_multi(): def run_app_multi():
app = RestServiceContainer((HomeApp(),Module1(),Module2())) app = RESTResource((HomeApp(),Module1(),Module2()))
app.run(8081) app.run(8081)
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -3,8 +3,12 @@ Server tests ...@@ -3,8 +3,12 @@ Server tests
@author: jacekf @author: jacekf
''' '''
from corepost import Response from corepost import Response, NotFoundException, AlreadyExistsException
from corepost.web import RestServiceContainer from corepost.web import RESTResource, route, Http
from twisted.python import log
import sys
#log.startLogging(sys.stdout)
class DB(): class DB():
"""Fake in-memory DB for testing""" """Fake in-memory DB for testing"""
...@@ -14,7 +18,7 @@ class Customer(): ...@@ -14,7 +18,7 @@ class Customer():
"""Represents customer entity""" """Represents customer entity"""
def __init__(self,customerId,firstName,lastName): def __init__(self,customerId,firstName,lastName):
(self.customerId,self.firstName,self.lastName) = (customerId,firstName,lastName) (self.customerId,self.firstName,self.lastName) = (customerId,firstName,lastName)
self.addresses = {} self.addresses = []
class CustomerAddress(): class CustomerAddress():
"""Represents customer address entity""" """Represents customer address entity"""
...@@ -24,76 +28,49 @@ class CustomerAddress(): ...@@ -24,76 +28,49 @@ class CustomerAddress():
class CustomerRestService(): class CustomerRestService():
path = "/customer" path = "/customer"
@route("/")
def getAll(self,request): def getAll(self,request):
return DB.customers return DB.customers.values()
@route("/<customerId>")
def get(self,request,customerId): def get(self,request,customerId):
return DB.customers[customerId] if customerId in DB.customers else Response(404, "Customer %s not found" % customerId)
def post(self,request,customerId,firstName,lastName):
if customerId in DB.customers:
return Response(409,"Customer %s already exists" % customerId)
else:
DB.customers[customerId] = Customer(customerId, firstName, lastName)
return Response(201)
def put(self,request,customerId,firstName,lastName):
if customerId in DB.customers: if customerId in DB.customers:
DB.customers[customerId].firstName = firstName return DB.customers[customerId]
DB.customers[customerId].lastName = lastName
return Response(200)
else: else:
return Response(404, "Customer %s not found" % customerId) raise NotFoundException("Customer", customerId)
def delete(self,request,customerId):
if customerId in DB.customers:
del(DB.customers[customerId])
return Response(200)
else:
return Response(404, "Customer %s not found" % customerId)
def deleteAll(self,request):
DB.customers.clear()
return Response(200)
class CustomerAddressRestService():
path = "/customer/<customerId>/address"
def getAll(self,request,customerId):
return DB.customers
def get(self,request,customerId):
return DB.customers[customerId] if customerId in DB.customers else Response(404, "Customer %s not found" % customerId)
@route("/",Http.POST)
def post(self,request,customerId,firstName,lastName): def post(self,request,customerId,firstName,lastName):
if customerId in DB.customers: if customerId in DB.customers:
return Response(409,"Customer %s already exists" % customerId) raise AlreadyExistsException("Customer",customerId)
else: else:
DB.customers[customerId] = Customer(customerId, firstName, lastName) DB.customers[customerId] = Customer(customerId, firstName, lastName)
return Response(201) return Response(201)
@route("/<customerId>",Http.PUT)
def put(self,request,customerId,firstName,lastName): def put(self,request,customerId,firstName,lastName):
if customerId in DB.customers: if customerId in DB.customers:
DB.customers[customerId].firstName = firstName DB.customers[customerId].firstName = firstName
DB.customers[customerId].lastName = lastName DB.customers[customerId].lastName = lastName
return Response(200) return Response(200)
else: else:
return Response(404, "Customer %s not found" % customerId) raise NotFoundException("Customer", customerId)
@route("/<customerId>",Http.DELETE)
def delete(self,request,customerId): def delete(self,request,customerId):
if customerId in DB.customers: if customerId in DB.customers:
del(DB.customers[customerId]) del(DB.customers[customerId])
return Response(200) return Response(200)
else: else:
return Response(404, "Customer %s not found" % customerId) raise NotFoundException("Customer", customerId)
@route("/",Http.DELETE)
def deleteAll(self,request): def deleteAll(self,request):
DB.customers.clear() DB.customers.clear()
return Response(200) return Response(200)
def run_rest_app(): def run_rest_app():
app = RestServiceContainer(restServices=(CustomerRestService(),)) app = RESTResource((CustomerRestService(),))
app.run(8085) app.run(8085)
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -11,8 +11,9 @@ from corepost.test.home_resource import run_app_home ...@@ -11,8 +11,9 @@ from corepost.test.home_resource import run_app_home
from corepost.test.multi_resource import run_app_multi from corepost.test.multi_resource import run_app_multi
from corepost.test.arguments import run_app_arguments from corepost.test.arguments import run_app_arguments
from corepost.test.filter_resource import run_filter_app from corepost.test.filter_resource import run_filter_app
from corepost.test.rest_resource import run_rest_app
apps = {'home_resource' : run_app_home,'multi_resource':run_app_multi,'arguments':run_app_arguments, 'filter_resource':run_filter_app} apps = {'home_resource' : run_app_home,'multi_resource':run_app_multi,'arguments':run_app_arguments, 'filter_resource':run_filter_app,'rest_resource':run_rest_app}
NULL = 'None' NULL = 'None'
......
...@@ -3,7 +3,7 @@ Main server classes ...@@ -3,7 +3,7 @@ Main server classes
@author: jacekf @author: jacekf
''' '''
from corepost import Response, IRestServiceContainer from corepost import Response, IRESTResource
from corepost.enums import Http from corepost.enums import Http
from corepost.routing import UrlRouter, RequestRouter from corepost.routing import UrlRouter, RequestRouter
from enums import MediaType from enums import MediaType
...@@ -20,12 +20,12 @@ from zope.interface import implements ...@@ -20,12 +20,12 @@ from zope.interface import implements
# #
######################################################### #########################################################
class RestServiceContainer(Resource): class RESTResource(Resource):
''' '''
Main resource responsible for routing REST requests to the implementing methods Main resource responsible for routing REST requests to the implementing methods
''' '''
isLeaf = True isLeaf = True
implements(IRestServiceContainer) implements(IRESTResource)
def __init__(self,services=(),schema=None,filters=()): def __init__(self,services=(),schema=None,filters=()):
''' '''
......
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