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
#
#########################################################
class IRestServiceContainer(Interface):
class IRESTResource(Interface):
"""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")
......@@ -27,8 +27,28 @@ class Response:
"""
def __init__(self,code=200,entity=None,headers={}):
self.code = code
self.entity=entity
self.entity=entity if entity != None else ""
self.headers=headers
def __str__(self):
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
Common routing classes, regardless of whether used in HTTP or multiprocess context
'''
from collections import defaultdict
from corepost import Response
from corepost import Response, RESTException
from corepost.enums import Http, HttpHeader
from corepost.utils import getMandatoryArgumentNames, convertToJson
from corepost.convert import convertForSerialization, generateXml
......@@ -16,7 +16,8 @@ from twisted.internet import reactor, defer
from twisted.web.http import parse_qs
from twisted.web.resource import Resource
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.ElementTree import Element
from zope.interface.verify import verifyObject
......@@ -264,6 +265,7 @@ class RequestRouter:
urlRouter = urlRouterInstance.urlRouter
val = urlRouter.call(urlRouterInstance.clazz,request,**allargs)
#handle Deferreds natively
if isinstance(val,defer.Deferred):
# add callback to finish the request
......@@ -281,14 +283,27 @@ class RequestRouter:
response = self.__generateResponse(request, val, request.code)
except exceptions.TypeError as ex:
log.err(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:
log.err(ex)
response = self.__createErrorResponse(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex))
else:
log.msg(ex,logLevel=logging.WARN)
response = self.__createErrorResponse(request,404,"URL '%s' not found\n" % request.path)
except Exception as ex:
log.err(ex)
response = self.__createErrorResponse(request,500,"Internal server error: %s" % ex)
# response handling
......
......@@ -3,7 +3,7 @@ Argument extraction tests
@author: jacekf
'''
from corepost.web import RestServiceContainer, validate, route
from corepost.web import RESTResource, validate, route
from corepost.enums import Http
from formencode import Schema, validators
......@@ -29,5 +29,5 @@ class ArgumentApp():
return "%s - %s - %s" % (rootId,childId,kwargs)
def run_app_arguments():
app = RestServiceContainer((ArgumentApp(),))
app = RESTResource((ArgumentApp(),))
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
@author: jacekf
'''
from corepost.web import RestServiceContainer, route
from corepost.web import RESTResource, route
from corepost.enums import Http
from corepost.filters import IRequestFilter, IResponseFilter
from zope.interface import implements
......@@ -41,7 +41,7 @@ class FilterService():
return request.received_headers
def run_filter_app():
app = RestServiceContainer(services=(FilterService(),),filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app = RESTResource(services=(FilterService(),),filters=(Change404to503Filter(),AddCustomHeaderFilter(),WrapAroundFilter(),))
app.run(8083)
if __name__ == "__main__":
......
......@@ -3,7 +3,7 @@ Server tests
@author: jacekf
'''
from corepost.web import RestServiceContainer, route
from corepost.web import RESTResource, route
from corepost.enums import Http, MediaType, HttpHeader
from twisted.internet import defer
from xml.etree import ElementTree
......@@ -115,7 +115,7 @@ class HomeApp():
return self.issue1
def run_app_home():
app = RestServiceContainer((HomeApp(),))
app = RESTResource((HomeApp(),))
app.run()
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
class HomeApp():
......@@ -34,7 +34,7 @@ class Module2():
return request.path
def run_app_multi():
app = RestServiceContainer((HomeApp(),Module1(),Module2()))
app = RESTResource((HomeApp(),Module1(),Module2()))
app.run(8081)
if __name__ == "__main__":
......
......@@ -3,8 +3,12 @@ Server tests
@author: jacekf
'''
from corepost import Response
from corepost.web import RestServiceContainer
from corepost import Response, NotFoundException, AlreadyExistsException
from corepost.web import RESTResource, route, Http
from twisted.python import log
import sys
#log.startLogging(sys.stdout)
class DB():
"""Fake in-memory DB for testing"""
......@@ -14,7 +18,7 @@ class Customer():
"""Represents customer entity"""
def __init__(self,customerId,firstName,lastName):
(self.customerId,self.firstName,self.lastName) = (customerId,firstName,lastName)
self.addresses = {}
self.addresses = []
class CustomerAddress():
"""Represents customer address entity"""
......@@ -24,76 +28,49 @@ class CustomerAddress():
class CustomerRestService():
path = "/customer"
@route("/")
def getAll(self,request):
return DB.customers
return DB.customers.values()
@route("/<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:
DB.customers[customerId].firstName = firstName
DB.customers[customerId].lastName = lastName
return Response(200)
return DB.customers[customerId]
else:
return Response(404, "Customer %s not found" % 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)
raise NotFoundException("Customer", customerId)
@route("/",Http.POST)
def post(self,request,customerId,firstName,lastName):
if customerId in DB.customers:
return Response(409,"Customer %s already exists" % customerId)
raise AlreadyExistsException("Customer",customerId)
else:
DB.customers[customerId] = Customer(customerId, firstName, lastName)
return Response(201)
@route("/<customerId>",Http.PUT)
def put(self,request,customerId,firstName,lastName):
if customerId in DB.customers:
DB.customers[customerId].firstName = firstName
DB.customers[customerId].lastName = lastName
return Response(200)
else:
return Response(404, "Customer %s not found" % customerId)
raise NotFoundException("Customer", customerId)
@route("/<customerId>",Http.DELETE)
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)
raise NotFoundException("Customer", customerId)
@route("/",Http.DELETE)
def deleteAll(self,request):
DB.customers.clear()
return Response(200)
def run_rest_app():
app = RestServiceContainer(restServices=(CustomerRestService(),))
app = RESTResource((CustomerRestService(),))
app.run(8085)
if __name__ == "__main__":
......
......@@ -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.arguments import run_app_arguments
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'
......
......@@ -3,7 +3,7 @@ Main server classes
@author: jacekf
'''
from corepost import Response, IRestServiceContainer
from corepost import Response, IRESTResource
from corepost.enums import Http
from corepost.routing import UrlRouter, RequestRouter
from enums import MediaType
......@@ -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
'''
isLeaf = True
implements(IRestServiceContainer)
implements(IRESTResource)
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