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

routing by incoming content type works

parent 89c4bf7c
......@@ -5,7 +5,7 @@ Feature: Arguments
CorePost should be able to correctly extract arguments
from paths, query arguments and form arguments
@path_arguments
@arguments_ok
Scenario Outline: Path argument extraction
Given 'arguments' is running
When as user 'None:None' I GET 'http://127.0.0.1:8082<url>'
......@@ -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/23 | 200 | [(<type 'int'>, 1), (<type 'float'>, 1.0), (<type 'str'>, '23')] |
@path_arguments
@arguments_error
Scenario Outline: Path argument extraction - error handling
Given 'arguments' is running
When as user 'None:None' I GET 'http://127.0.0.1:8082<url>'
......
......@@ -6,9 +6,11 @@ Feature: Content types
correctly parse/generate
JSON/XML/YAML based on content types
Background:
Given 'home_resource' is running
@json
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
"""
{"test":"test2"}
......@@ -26,7 +28,6 @@ Feature: Content types
@json
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
"""
wrong_json
......@@ -41,7 +42,6 @@ Feature: Content types
@xml
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
"""
<root><test>TEST</test><test2>Yo</test2></root>
......@@ -57,7 +57,6 @@ Feature: Content types
@xml
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
"""
wrong xml
......@@ -73,7 +72,6 @@ Feature: Content types
@yaml
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
"""
invoice: 34843
......@@ -144,7 +142,6 @@ total: 4443.52
@yaml
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
"""
- test
......@@ -157,3 +154,22 @@ total: 4443.52
| method |
| POST |
| PUT |
@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
'''
from corepost.web import CorePost, route
from corepost.enums import Http
from corepost.enums import Http, MediaType, HttpHeader
from twisted.internet import defer
from xml.etree import ElementTree
import json, yaml
......@@ -54,6 +54,25 @@ class HomeApp(CorePost):
def test_yaml(self,request,**kwargs):
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():
app = HomeApp()
......
......@@ -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.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*$")
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.follow_redirects = False
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
scc.http_headers['Content-type'] = 'text/xml'
elif request_type == "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 '(.*)' = '(.*)'")
def when_i_define_http_header_with_value(header,value):
......
......@@ -11,3 +11,6 @@ def getMandatoryArgumentNames(f):
else:
return args[0:len(args) - len(defaults)]
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:
def __init__(self,f,url,methods,accepts,produces,cache):
self.__url = url
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.__cache = cache
self.__f = f
......@@ -66,6 +66,10 @@ class RequestRouter:
def url(self):
return self.__url
@property
def accepts(self):
return self.__accepts
def addValidator(self,fieldName,validator):
'''Adds additional field-specific formencode validators'''
self.__validators[fieldName] = validator
......@@ -123,8 +127,8 @@ class CorePost(Resource):
Constructor
'''
Resource.__init__(self)
self.__urls = defaultdict(dict)
self.__cachedUrls = defaultdict(dict)
self.__urls = {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)}
self.__routers = {}
self.__schema = schema
self.__registerRouters()
......@@ -139,7 +143,8 @@ class CorePost(Resource):
if type(func) == FunctionType and hasattr(func,'corepostRequestRouter'):
rq = func.corepostRequestRouter
for method in rq.methods:
self.__urls[method][rq.url] = rq
for accepts in rq.accepts:
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):
......@@ -165,20 +170,36 @@ class CorePost(Resource):
def __renderUrl(self,request):
"""Finds the appropriate router and dispatches the request to the registered function"""
# see if already cached
try:
path = '/' + '/'.join(request.postpath)
contentType = MediaType.WILDCARD if HttpHeader.CONTENT_TYPE not in request.received_headers else request.received_headers[HttpHeader.CONTENT_TYPE]
urlrouter, pathargs = None, None
if path in self.__cachedUrls[request.method]:
cachedUrl = self.__cachedUrls[request.method][path]
# fetch URL arguments <-> function from cache if hit at least once before
if contentType in self.__cachedUrls[request.method][path]:
cachedUrl = self.__cachedUrls[request.method][path][contentType]
urlrouter,pathargs = cachedUrl.router, cachedUrl.args
else:
# first time this URL is called
for router in self.__urls[request.method].values():
router = None
for contentTypeFunctions in self.__urls[request.method].values():
if contentType in contentTypeFunctions:
# there is an exact function for this incoming content type
router = contentTypeFunctions[contentType]
elif MediaType.WILDCARD in contentTypeFunctions:
# fall back to any wildcard method
router = contentTypeFunctions[MediaType.WILDCARD]
if router != None:
# see if the path arguments match up against any function @route definition
args = router.getArguments(path)
if args != None:
if router.cache:
self.__cachedUrls[request.method][path] = CachedUrl(router, args)
self.__cachedUrls[request.method][path][contentType] = CachedUrl(router, args)
urlrouter,pathargs = router,args
break
#actual call
if urlrouter != None and pathargs != None:
......@@ -224,6 +245,9 @@ class CorePost(Resource):
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):
"""Common method for rendering errors"""
request.setResponseCode(code)
......
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