Commit 29c6715b authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

routing by incoming content type works

parent 89c4bf7c
...@@ -5,7 +5,7 @@ Feature: Arguments ...@@ -5,7 +5,7 @@ Feature: Arguments
CorePost should be able to correctly extract arguments CorePost should be able to correctly extract arguments
from paths, query arguments and form arguments from paths, query arguments and form arguments
@path_arguments @arguments_ok
Scenario Outline: Path argument extraction Scenario Outline: Path argument extraction
Given 'arguments' is running Given 'arguments' is running
When as user 'None:None' I GET 'http://127.0.0.1:8082<url>' When as user 'None:None' I GET 'http://127.0.0.1:8082<url>'
...@@ -18,7 +18,7 @@ Feature: Arguments ...@@ -18,7 +18,7 @@ Feature: Arguments
| /int/1/float/1/string/TEST | 200 | [(<type 'int'>, 1), (<type 'float'>, 1.0), (<type 'str'>, 'TEST')] | | /int/1/float/1/string/TEST | 200 | [(<type 'int'>, 1), (<type 'float'>, 1.0), (<type 'str'>, 'TEST')] |
| /int/1/float/1/string/23 | 200 | [(<type 'int'>, 1), (<type 'float'>, 1.0), (<type 'str'>, '23')] | | /int/1/float/1/string/23 | 200 | [(<type 'int'>, 1), (<type 'float'>, 1.0), (<type 'str'>, '23')] |
@path_arguments @arguments_error
Scenario Outline: Path argument extraction - error handling Scenario Outline: Path argument extraction - error handling
Given 'arguments' is running Given 'arguments' is running
When as user 'None:None' I GET 'http://127.0.0.1:8082<url>' When as user 'None:None' I GET 'http://127.0.0.1:8082<url>'
......
...@@ -6,9 +6,11 @@ Feature: Content types ...@@ -6,9 +6,11 @@ Feature: Content types
correctly parse/generate correctly parse/generate
JSON/XML/YAML based on content types JSON/XML/YAML based on content types
Background:
Given 'home_resource' is running
@json @json
Scenario Outline: Parse incoming JSON data Scenario Outline: Parse incoming JSON data
Given 'home_resource' is running
When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/json' with JSON When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/json' with JSON
""" """
{"test":"test2"} {"test":"test2"}
...@@ -26,7 +28,6 @@ Feature: Content types ...@@ -26,7 +28,6 @@ Feature: Content types
@json @json
Scenario Outline: Handle invalid incoming JSON data Scenario Outline: Handle invalid incoming JSON data
Given 'home_resource' is running
When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/json' with JSON When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/json' with JSON
""" """
wrong_json wrong_json
...@@ -41,7 +42,6 @@ Feature: Content types ...@@ -41,7 +42,6 @@ Feature: Content types
@xml @xml
Scenario Outline: Parse incoming XML data Scenario Outline: Parse incoming XML data
Given 'home_resource' is running
When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/xml' with XML When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/xml' with XML
""" """
<root><test>TEST</test><test2>Yo</test2></root> <root><test>TEST</test><test2>Yo</test2></root>
...@@ -57,7 +57,6 @@ Feature: Content types ...@@ -57,7 +57,6 @@ Feature: Content types
@xml @xml
Scenario Outline: Handle invalid XML data Scenario Outline: Handle invalid XML data
Given 'home_resource' is running
When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/xml' with XML When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/xml' with XML
""" """
wrong xml wrong xml
...@@ -73,7 +72,6 @@ Feature: Content types ...@@ -73,7 +72,6 @@ Feature: Content types
@yaml @yaml
Scenario Outline: Parse incoming YAML data Scenario Outline: Parse incoming YAML data
Given 'home_resource' is running
When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/yaml' with YAML When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/yaml' with YAML
""" """
invoice: 34843 invoice: 34843
...@@ -144,7 +142,6 @@ total: 4443.52 ...@@ -144,7 +142,6 @@ total: 4443.52
@yaml @yaml
Scenario Outline: Handle invalid YAML data Scenario Outline: Handle invalid YAML data
Given 'home_resource' is running
When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/yaml' with YAML When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/yaml' with YAML
""" """
- test - test
...@@ -156,4 +153,23 @@ total: 4443.52 ...@@ -156,4 +153,23 @@ total: 4443.52
Examples: Examples:
| method | | method |
| POST | | POST |
| PUT | | PUT |
\ No newline at end of file
@json @yaml @xml @route_content_type
Scenario Outline: Route by incoming content type
When I prepare HTTP header 'content-type' = '<content>'
When as user 'None:None' I <method> 'http://127.0.0.1:8080/post/by/content' with <type> body '<body>'
Then I expect HTTP code <code>
And I expect content contains '<content>'
Examples:
| method | type | body | content | code |
| POST | JSON | {"test":2} | application/json | 201 |
| POST | XML | <test>1</test> | application/xml | 201 |
| POST | XML | <test>1</test> | text/xml | 201 |
| POST | YAML | test: 2 | text/yaml | 201 |
| PUT | JSON | {"test":2} | application/json | 200 |
| PUT | XML | <test>1</test> | text/xml | 200 |
| PUT | XML | <test>1</test> | application/xml | 200 |
| PUT | YAML | test: 2 | text/yaml | 200 |
\ No newline at end of file
...@@ -4,7 +4,7 @@ Server tests ...@@ -4,7 +4,7 @@ Server tests
''' '''
from corepost.web import CorePost, route from corepost.web import CorePost, route
from corepost.enums import Http 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
import json, yaml import json, yaml
...@@ -54,6 +54,25 @@ class HomeApp(CorePost): ...@@ -54,6 +54,25 @@ class HomeApp(CorePost):
def test_yaml(self,request,**kwargs): def test_yaml(self,request,**kwargs):
return "%s" % yaml.dump(request.yaml,indent=4,width=130,default_flow_style=False) return "%s" % yaml.dump(request.yaml,indent=4,width=130,default_flow_style=False)
##################################################################
# same URLs, routed by incoming content type
###################################################################
@route("/post/by/content",(Http.POST,Http.PUT),MediaType.APPLICATION_JSON)
def test_content_app_json(self,request,**kwargs):
return request.received_headers[HttpHeader.CONTENT_TYPE]
@route("/post/by/content",(Http.POST,Http.PUT),(MediaType.TEXT_XML,MediaType.APPLICATION_XML))
def test_content_xml(self,request,**kwargs):
return request.received_headers[HttpHeader.CONTENT_TYPE]
@route("/post/by/content",(Http.POST,Http.PUT),MediaType.TEXT_YAML)
def test_content_yaml(self,request,**kwargs):
return request.received_headers[HttpHeader.CONTENT_TYPE]
@route("/post/by/content",(Http.POST,Http.PUT))
def test_content_catch_all(self,request,**kwargs):
return MediaType.WILDCARD
def run_app_home(): def run_app_home():
app = HomeApp() app = HomeApp()
......
...@@ -68,8 +68,12 @@ def when_as_user_i_send_post_put_to_url(user,password,method,url,params): ...@@ -68,8 +68,12 @@ def when_as_user_i_send_post_put_to_url(user,password,method,url,params):
scc.http_headers['Content-type'] = 'application/x-www-form-urlencoded' scc.http_headers['Content-type'] = 'application/x-www-form-urlencoded'
scc.response, scc.content = h.request(url, method, urlencode(as_dict(params)), headers = scc.http_headers) scc.response, scc.content = h.request(url, method, urlencode(as_dict(params)), headers = scc.http_headers)
@When(r"^as user '(.+):(.+)' I (POST|PUT) '(.+)' with (XML|JSON|YAML) body '(.+)'\s*$")
def when_as_user_i_send_post_put_xml_json_to_url(user,password,method,url,request_type,body):
when_as_user_i_send_post_put_xml_json_to_url_multiline(body,user,password,method,url,request_type)
@When(r"^as user '(.+):(.+)' I (POST|PUT) '(.+)' with (XML|JSON|YAML)\s*$") @When(r"^as user '(.+):(.+)' I (POST|PUT) '(.+)' with (XML|JSON|YAML)\s*$")
def when_as_user_i_send_post_put_xml_json_to_url(payload,user,password,method,url,request_type): def when_as_user_i_send_post_put_xml_json_to_url_multiline(body,user,password,method,url,request_type):
h = httplib2.Http() h = httplib2.Http()
h.follow_redirects = False h.follow_redirects = False
h.add_credentials(user, password) h.add_credentials(user, password)
...@@ -79,7 +83,7 @@ def when_as_user_i_send_post_put_xml_json_to_url(payload,user,password,method,ur ...@@ -79,7 +83,7 @@ def when_as_user_i_send_post_put_xml_json_to_url(payload,user,password,method,ur
scc.http_headers['Content-type'] = 'text/xml' scc.http_headers['Content-type'] = 'text/xml'
elif request_type == "YAML": elif request_type == "YAML":
scc.http_headers['Content-type'] = 'text/yaml' scc.http_headers['Content-type'] = 'text/yaml'
scc.response, scc.content = h.request(url, method, payload, headers = scc.http_headers) scc.response, scc.content = h.request(url, method, body, headers = scc.http_headers)
@When("I prepare HTTP header '(.*)' = '(.*)'") @When("I prepare HTTP header '(.*)' = '(.*)'")
def when_i_define_http_header_with_value(header,value): def when_i_define_http_header_with_value(header,value):
......
...@@ -10,4 +10,7 @@ def getMandatoryArgumentNames(f): ...@@ -10,4 +10,7 @@ def getMandatoryArgumentNames(f):
return args return args
else: else:
return args[0:len(args) - len(defaults)] return args[0:len(args) - len(defaults)]
\ No newline at end of file def getRouterKey(method,url):
'''Returns the common key used to represent a function that a request can be routed to'''
return "%s %s" % (method,url)
\ No newline at end of file
...@@ -27,7 +27,7 @@ class RequestRouter: ...@@ -27,7 +27,7 @@ class RequestRouter:
def __init__(self,f,url,methods,accepts,produces,cache): def __init__(self,f,url,methods,accepts,produces,cache):
self.__url = url self.__url = url
self.__methods = methods if isinstance(methods,tuple) else (methods,) self.__methods = methods if isinstance(methods,tuple) else (methods,)
self.__accepts = accepts self.__accepts = accepts if isinstance(accepts,tuple) else (accepts,)
self.__produces = produces self.__produces = produces
self.__cache = cache self.__cache = cache
self.__f = f self.__f = f
...@@ -66,6 +66,10 @@ class RequestRouter: ...@@ -66,6 +66,10 @@ class RequestRouter:
def url(self): def url(self):
return self.__url return self.__url
@property
def accepts(self):
return self.__accepts
def addValidator(self,fieldName,validator): def addValidator(self,fieldName,validator):
'''Adds additional field-specific formencode validators''' '''Adds additional field-specific formencode validators'''
self.__validators[fieldName] = validator self.__validators[fieldName] = validator
...@@ -123,8 +127,8 @@ class CorePost(Resource): ...@@ -123,8 +127,8 @@ class CorePost(Resource):
Constructor Constructor
''' '''
Resource.__init__(self) Resource.__init__(self)
self.__urls = defaultdict(dict) self.__urls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict)}
self.__cachedUrls = defaultdict(dict) self.__cachedUrls = {Http.GET: defaultdict(dict),Http.POST: defaultdict(dict),Http.PUT: defaultdict(dict),Http.DELETE: defaultdict(dict)}
self.__routers = {} self.__routers = {}
self.__schema = schema self.__schema = schema
self.__registerRouters() self.__registerRouters()
...@@ -139,8 +143,9 @@ class CorePost(Resource): ...@@ -139,8 +143,9 @@ class CorePost(Resource):
if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'): if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'):
rq = func.corepostRequestRouter rq = func.corepostRequestRouter
for method in rq.methods: for method in rq.methods:
self.__urls[method][rq.url] = rq for accepts in rq.accepts:
self.__routers[func] = rq # needed so that we can lookup the router for a specific function self.__urls[method][rq.url][accepts] = rq
self.__routers[func] = rq # needed so that we can lookup the router for a specific function
def route(self,url,methods=(Http.GET,),accepts=MediaType.WILDCARD,produces=None,cache=True): def route(self,url,methods=(Http.GET,),accepts=MediaType.WILDCARD,produces=None,cache=True):
'''Obsolete''' '''Obsolete'''
...@@ -165,64 +170,83 @@ class CorePost(Resource): ...@@ -165,64 +170,83 @@ class CorePost(Resource):
def __renderUrl(self,request): def __renderUrl(self,request):
"""Finds the appropriate router and dispatches the request to the registered function""" """Finds the appropriate router and dispatches the request to the registered function"""
# see if already cached # see if already cached
path = '/' + '/'.join(request.postpath) try:
path = '/' + '/'.join(request.postpath)
urlrouter, pathargs = None, None contentType = MediaType.WILDCARD if HttpHeader.CONTENT_TYPE not in request.received_headers else request.received_headers[HttpHeader.CONTENT_TYPE]
if path in self.__cachedUrls[request.method]:
cachedUrl = self.__cachedUrls[request.method][path]
urlrouter,pathargs = cachedUrl.router, cachedUrl.args
else:
# first time this URL is called
for router in self.__urls[request.method].values():
args = router.getArguments(path)
if args != None:
if router.cache:
self.__cachedUrls[request.method][path] = CachedUrl(router, args)
urlrouter,pathargs = router,args
#actual call urlrouter, pathargs = None, None
if urlrouter != None and pathargs != None: # fetch URL arguments <-> function from cache if hit at least once before
allargs = copy.deepcopy(pathargs) if contentType in self.__cachedUrls[request.method][path]:
# handler for weird Twisted logic where PUT does not get form params cachedUrl = self.__cachedUrls[request.method][path][contentType]
# see: http://twistedmatrix.com/pipermail/twisted-web/2007-March/003338.html urlrouter,pathargs = cachedUrl.router, cachedUrl.args
requestargs = request.args else:
if request.method == Http.PUT and HttpHeader.CONTENT_TYPE in request.received_headers.keys() \ # first time this URL is called
and request.received_headers[HttpHeader.CONTENT_TYPE] == MediaType.APPLICATION_FORM_URLENCODED: router = None
requestargs = parse_qs(request.content.read(), 1)
#merge form args for contentTypeFunctions in self.__urls[request.method].values():
for arg in requestargs.keys():
# maintain first instance of an argument always if contentType in contentTypeFunctions:
if arg not in allargs: # there is an exact function for this incoming content type
allargs[arg] = requestargs[arg][0] router = contentTypeFunctions[contentType]
elif MediaType.WILDCARD in contentTypeFunctions:
try: # fall back to any wildcard method
# if POST/PUT, check if we need to automatically parse JSON router = contentTypeFunctions[MediaType.WILDCARD]
self.__parseRequestData(request)
if router != None:
val = urlrouter.call(self,request,**allargs) # see if the path arguments match up against any function @route definition
args = router.getArguments(path)
#handle Deferreds natively if args != None:
if isinstance(val,defer.Deferred): if router.cache:
# we assume the method will call request.finish() self.__cachedUrls[request.method][path][contentType] = CachedUrl(router, args)
return NOT_DONE_YET urlrouter,pathargs = router,args
else: break
#special logic for POST to return 201 (created)
if request.method == Http.POST: #actual call
if hasattr(request, 'code'): if urlrouter != None and pathargs != None:
if request.code == 200: allargs = copy.deepcopy(pathargs)
# handler for weird Twisted logic where PUT does not get form params
# see: http://twistedmatrix.com/pipermail/twisted-web/2007-March/003338.html
requestargs = request.args
if request.method == Http.PUT and HttpHeader.CONTENT_TYPE in request.received_headers.keys() \
and request.received_headers[HttpHeader.CONTENT_TYPE] == MediaType.APPLICATION_FORM_URLENCODED:
requestargs = parse_qs(request.content.read(), 1)
#merge form args
for arg in requestargs.keys():
# maintain first instance of an argument always
if arg not in allargs:
allargs[arg] = requestargs[arg][0]
try:
# if POST/PUT, check if we need to automatically parse JSON
self.__parseRequestData(request)
val = urlrouter.call(self,request,**allargs)
#handle Deferreds natively
if isinstance(val,defer.Deferred):
# we assume the method will call request.finish()
return NOT_DONE_YET
else:
#special logic for POST to return 201 (created)
if request.method == Http.POST:
if hasattr(request, 'code'):
if request.code == 200:
request.setResponseCode(201)
else:
request.setResponseCode(201) request.setResponseCode(201)
else:
request.setResponseCode(201) return val
except exceptions.TypeError as ex:
return val return self.__renderError(request,400,"%s" % ex)
except exceptions.TypeError as ex: except Exception as ex:
return self.__renderError(request,400,"%s" % ex) return self.__renderError(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex))
except Exception as ex:
return self.__renderError(request,500,"Unexpected server error: %s\n%s" % (type(ex),ex)) else:
return self.__renderError(request,404,"URL '%s' not found\n" % request.path)
else:
return self.__renderError(request,404,"URL '%s' not found\n" % request.path) except Exception as ex:
return self.__renderError(request,500,"Internal server error: %s" % ex)
def __renderError(self,request,code,message): def __renderError(self,request,code,message):
"""Common method for rendering errors""" """Common method for rendering errors"""
......
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