Commit 5dc9e4cb authored by Jacek Furmankiewicz's avatar Jacek Furmankiewicz

POST returns 201 by default, missing mandatory args throw 400, initial validator investigation

parent 31c6946f
...@@ -5,6 +5,7 @@ Argument extraction tests ...@@ -5,6 +5,7 @@ Argument extraction tests
from corepost.web import CorePost from corepost.web import CorePost
from corepost.enums import Http from corepost.enums import Http
from formencode import Schema, validators
app = CorePost() app = CorePost()
...@@ -13,5 +14,10 @@ def test(request,intarg,floatarg,stringarg,**kwargs): ...@@ -13,5 +14,10 @@ def test(request,intarg,floatarg,stringarg,**kwargs):
args = (intarg,floatarg,stringarg) args = (intarg,floatarg,stringarg)
return "%s" % map(lambda x: (type(x),x),args) return "%s" % map(lambda x: (type(x),x),args)
@app.route("/validate/<int:rootId>/children",Http.POST)
@app.validate(childid=validators.String(not_empty=True))
def post(request,rootId,childId,**kwargs):
return "%s - %s - %s" % (rootId,childId,kwargs)
def run_app_arguments(): def run_app_arguments():
app.run(8082) app.run(8082)
\ No newline at end of file
...@@ -26,7 +26,7 @@ Feature: URL routing ...@@ -26,7 +26,7 @@ Feature: URL routing
Scenario: Single resource - POST Scenario: Single resource - POST
Given 'home_resource' is running Given 'home_resource' is running
When as user 'None:None' I POST 'http://127.0.0.1:8080/post' with 'test=value&test2=value2' When as user 'None:None' I POST 'http://127.0.0.1:8080/post' with 'test=value&test2=value2'
Then I expect HTTP code 200 Then I expect HTTP code 201
And I expect content contains '{'test': 'value', 'test2': 'value2'}' And I expect content contains '{'test': 'value', 'test2': 'value2'}'
@single @single_put @single @single_put
...@@ -46,9 +46,11 @@ Feature: URL routing ...@@ -46,9 +46,11 @@ Feature: URL routing
Scenario: Single resource - multiple methods at same URL Scenario: Single resource - multiple methods at same URL
Given 'home_resource' is running Given 'home_resource' is running
When as user 'None:None' I POST 'http://127.0.0.1:8080/postput' with 'test=value&test2=value2' When as user 'None:None' I POST 'http://127.0.0.1:8080/postput' with 'test=value&test2=value2'
Then I expect HTTP code 200 # POST return 201 by default
Then I expect HTTP code 201
And I expect content contains '{'test': 'value', 'test2': 'value2'}' And I expect content contains '{'test': 'value', 'test2': 'value2'}'
When as user 'None:None' I PUT 'http://127.0.0.1:8080/postput' with 'test=value&test2=value2' When as user 'None:None' I PUT 'http://127.0.0.1:8080/postput' with 'test=value&test2=value2'
# PUT return 201 by default
Then I expect HTTP code 200 Then I expect HTTP code 200
And I expect content contains '{'test': 'value', 'test2': 'value2'}' And I expect content contains '{'test': 'value', 'test2': 'value2'}'
......
Using step definitions from: '../steps'
@validate
Feature: Argument Validators
CorePost should be able to correctly validate path, query and form arguments
@validate
Scenario Outline: Path argument extraction
Given 'arguments' is running
When as user 'None:None' I POST 'http://127.0.0.1:8082/validate/23/children' with '<args>'
Then I expect HTTP code <code>
And I expect content contains '<content>'
Examples:
| args | code | content |
| childId=jacekf | 201 | 23 - jacekf - {} |
| childId=jacekf&otherId=test | 201 | 23 - jacekf - {'otherId': 'test'} |
\ No newline at end of file
'''
Created on 2011-09-02
Misc tests
@author: jacekf
'''
def dec(f):
print "DEC"
def wrap():
print "WRAP"
v = f()
return v
return wrap
@dec
def test():
print "TEST3232"
if __name__ == "__main__":
test()
test()
test()
\ No newline at end of file
'''
Various CorePost utilities
'''
from inspect import getargspec
def getMandatoryArgumentNames(f):
'''Returns a tuple of the mandatory arguments required in a function'''
args,_,_,defaults = getargspec(f)
if defaults == None:
return args
else:
return args[0:len(args) - len(defaults)]
\ No newline at end of file
...@@ -3,7 +3,7 @@ Main server classes ...@@ -3,7 +3,7 @@ Main server classes
@author: jacekf @author: jacekf
''' '''
import re, copy import re, copy, exceptions
from twisted.internet import reactor, defer from twisted.internet import reactor, defer
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
...@@ -11,15 +11,17 @@ from twisted.web.http import parse_qs ...@@ -11,15 +11,17 @@ from twisted.web.http import parse_qs
from collections import defaultdict from collections import defaultdict
from enums import MediaType from enums import MediaType
from corepost.enums import Http from corepost.enums import Http
from corepost.utils import getMandatoryArgumentNames
from formencode import validators, Schema, Validator
class RequestRouter: class RequestRouter:
""" Common class for containing info related to routing a request to a function """ ''' Common class for containing info related to routing a request to a function '''
__urlMatcher = re.compile(r"<(int|float|):?([a-zA-Z0-9]+)>") __urlMatcher = re.compile(r"<(int|float|):?([a-zA-Z0-9]+)>")
__urlRegexReplace = {"":r"(?P<arg>.+)","int":r"(?P<arg>\d+)","float":r"(?P<arg>\d+.?\d*)"} __urlRegexReplace = {"":r"(?P<arg>.+)","int":r"(?P<arg>\d+)","float":r"(?P<arg>\d+.?\d*)"}
__typeConverters = {"int":int,"float":float} __typeConverters = {"int":int,"float":float}
def __init__(self,f,url,method,accepts,produces,cache, schema): def __init__(self,f,url,method,accepts,produces,cache):
self.__url = url self.__url = url
self.__method = method self.__method = method
self.__accepts = accepts self.__accepts = accepts
...@@ -27,7 +29,9 @@ class RequestRouter: ...@@ -27,7 +29,9 @@ class RequestRouter:
self.__cache = cache self.__cache = cache
self.__f = f self.__f = f
self.__argConverters = {} # dict of arg names -> group index self.__argConverters = {} # dict of arg names -> group index
self.__schema = schema self.__schema = None
self.__validators = {}
self.__mandatory = getMandatoryArgumentNames(f)[1:]
#parse URL into regex used for matching #parse URL into regex used for matching
m = RequestRouter.__urlMatcher.findall(url) m = RequestRouter.__urlMatcher.findall(url)
...@@ -49,19 +53,23 @@ class RequestRouter: ...@@ -49,19 +53,23 @@ class RequestRouter:
@property @property
def cache(self): def cache(self):
"""Indicates if this URL should be cached or not""" '''Indicates if this URL should be cached or not'''
return self.__cache return self.__cache
@property @property
def schema(self): def schema(self):
"""Returns the formencode Schema, if this URL uses custom validation schema""" ''''Returns the formencode Schema, if this URL uses custom validation schema'''
return self.__schema return self.__schema
def addValidator(self,fieldName,validator):
'''Adds additional field-specific formencode validators'''
self.__validators[fieldName] = validator
def getArguments(self,url): def getArguments(self,url):
""" '''
Returns None if nothing matched (i.e. URL does not match), empty dict if no args found (i,e, static URL) Returns None if nothing matched (i.e. URL does not match), empty dict if no args found (i,e, static URL)
or dict with arg/values for dynamic URLs or dict with arg/values for dynamic URLs
""" '''
g = self.__matcher.search(url) g = self.__matcher.search(url)
if g != None: if g != None:
args = g.groupdict() args = g.groupdict()
...@@ -76,14 +84,17 @@ class RequestRouter: ...@@ -76,14 +84,17 @@ class RequestRouter:
return None return None
def call(self,request,**kwargs): def call(self,request,**kwargs):
"""Forwards call to underlying method""" '''Forwards call to underlying method'''
for arg in self.__mandatory:
if arg not in kwargs:
raise TypeError("Missing mandatory argument '%s'" % arg)
return self.__f(request,**kwargs) return self.__f(request,**kwargs)
class CachedUrl(): class CachedUrl():
""" '''
Used for caching URLs that have been already routed once before. Avoids the overhead Used for caching URLs that have been already routed once before. Avoids the overhead
of regex processing on every incoming call for commonly accessed REST URLs of regex processing on every incoming call for commonly accessed REST URLs
""" '''
def __init__(self,router,args): def __init__(self,router,args):
self.__router = router self.__router = router
self.__args = args self.__args = args
...@@ -110,6 +121,7 @@ class CorePost(Resource): ...@@ -110,6 +121,7 @@ class CorePost(Resource):
self.__urls = defaultdict(dict) self.__urls = defaultdict(dict)
self.__cachedUrls = defaultdict(dict) self.__cachedUrls = defaultdict(dict)
self.__methods = {} self.__methods = {}
self.__routers = {}
self.__path = path self.__path = path
self.__schema = schema self.__schema = schema
...@@ -117,24 +129,34 @@ class CorePost(Resource): ...@@ -117,24 +129,34 @@ class CorePost(Resource):
def path(self): def path(self):
return self.__path return self.__path
def __registerFunction(self,f,url,methods,accepts,produces,cache,schema): def __registerFunction(self,f,url,methods,accepts,produces,cache):
if f not in self.__methods.values(): if f not in self.__methods.values():
if not isinstance(methods,(list,tuple)): if not isinstance(methods,(list,tuple)):
methods = (methods,) methods = (methods,)
for method in methods: for method in methods:
rq = RequestRouter(f, url, method, accepts, produces,cache,schema) rq = RequestRouter(f, url, method, accepts, produces,cache)
self.__urls[method][url] = rq self.__urls[method][url] = rq
self.__routers[f] = rq # needed so that we can lookup the router for a specific function
self.__methods[url] = f self.__methods[url] = f
def route(self,url,methods=(Http.GET,),accepts=MediaType.WILDCARD,produces=None,cache=True, schema=None): def route(self,url,methods=(Http.GET,),accepts=MediaType.WILDCARD,produces=None,cache=True):
"""Main decorator for registering REST functions """ """Main decorator for registering REST functions """
def wrap(f): def wrap(f,*args,**kwargs):
self.__registerFunction(f, url, methods, accepts, produces,cache, schema) self.__registerFunction(f, url, methods, accepts, produces,cache)
return f return f
return wrap return wrap
def validate(self,schema=None,**kwargs):
'''
Main decorator for registering additional validators for incoming URL arguments
'''
def wrap(f,**kwargs):
print kwargs
return f
return wrap
def render_GET(self,request): def render_GET(self,request):
""" Handles all GET requests """ """ Handles all GET requests """
return self.__renderUrl(request) return self.__renderUrl(request)
...@@ -187,27 +209,31 @@ class CorePost(Resource): ...@@ -187,27 +209,31 @@ class CorePost(Resource):
# if POST/PUT, check if we need to automatically parse JSON # if POST/PUT, check if we need to automatically parse JSON
# TODO # TODO
#validate input against formencode schema if defined
self.__validateArguments(urlrouter, request, allargs)
#handle Deferreds natively #handle Deferreds natively
val = urlrouter.call(request,**allargs) try:
if isinstance(val,defer.Deferred): val = urlrouter.call(request,**allargs)
# we assume the method will call request.finish()
return NOT_DONE_YET if isinstance(val,defer.Deferred):
else: # we assume the method will call request.finish()
return val 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)
return val
except exceptions.TypeError as ex:
return self.__renderError(request,400,"%s" % ex)
except Exception as ex:
return self.__renderError(request,500,"Unexpected server error: %s" % type(ex))
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)
def __validateArguments(self,urlrouter,request,allargs):
schema = urlrouter.schema if urlrouter.schema != None else self.__schema
if schema != None:
from formencode import Schema
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