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
...@@ -157,3 +154,22 @@ total: 4443.52 ...@@ -157,3 +154,22 @@ total: 4443.52
| method | | method |
| POST | | POST |
| PUT | | 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 ...@@ -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):
......
...@@ -11,3 +11,6 @@ def getMandatoryArgumentNames(f): ...@@ -11,3 +11,6 @@ def getMandatoryArgumentNames(f):
else: else:
return args[0:len(args) - len(defaults)] 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: ...@@ -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,7 +143,8 @@ class CorePost(Resource): ...@@ -139,7 +143,8 @@ 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.__urls[method][rq.url][accepts] = rq
self.__routers[func] = rq # needed so that we can lookup the router for a specific function 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):
...@@ -165,20 +170,36 @@ class CorePost(Resource): ...@@ -165,20 +170,36 @@ 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
try:
path = '/' + '/'.join(request.postpath) 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 urlrouter, pathargs = None, None
if path in self.__cachedUrls[request.method]: # fetch URL arguments <-> function from cache if hit at least once before
cachedUrl = self.__cachedUrls[request.method][path] if contentType in self.__cachedUrls[request.method][path]:
cachedUrl = self.__cachedUrls[request.method][path][contentType]
urlrouter,pathargs = cachedUrl.router, cachedUrl.args urlrouter,pathargs = cachedUrl.router, cachedUrl.args
else: else:
# first time this URL is called # 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) args = router.getArguments(path)
if args != None: if args != None:
if router.cache: if router.cache:
self.__cachedUrls[request.method][path] = CachedUrl(router, args) self.__cachedUrls[request.method][path][contentType] = CachedUrl(router, args)
urlrouter,pathargs = router,args urlrouter,pathargs = router,args
break
#actual call #actual call
if urlrouter != None and pathargs != None: if urlrouter != None and pathargs != None:
...@@ -224,6 +245,9 @@ class CorePost(Resource): ...@@ -224,6 +245,9 @@ class CorePost(Resource):
else: else:
return self.__renderError(request,404,"URL '%s' not found\n" % request.path) 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"""
request.setResponseCode(code) 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