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
from corepost.web import CorePost
from corepost.enums import Http
from formencode import Schema, validators
app = CorePost()
......@@ -13,5 +14,10 @@ def test(request,intarg,floatarg,stringarg,**kwargs):
args = (intarg,floatarg,stringarg)
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():
app.run(8082)
\ No newline at end of file
......@@ -26,7 +26,7 @@ Feature: URL routing
Scenario: Single resource - POST
Given 'home_resource' is running
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'}'
@single @single_put
......@@ -46,9 +46,11 @@ Feature: URL routing
Scenario: Single resource - multiple methods at same URL
Given 'home_resource' is running
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'}'
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
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
@author: jacekf
'''
import re, copy
import re, copy, exceptions
from twisted.internet import reactor, defer
from twisted.web.resource import Resource
from twisted.web.server import Site, NOT_DONE_YET
......@@ -11,15 +11,17 @@ from twisted.web.http import parse_qs
from collections import defaultdict
from enums import MediaType
from corepost.enums import Http
from corepost.utils import getMandatoryArgumentNames
from formencode import validators, Schema, Validator
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]+)>")
__urlRegexReplace = {"":r"(?P<arg>.+)","int":r"(?P<arg>\d+)","float":r"(?P<arg>\d+.?\d*)"}
__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.__method = method
self.__accepts = accepts
......@@ -27,7 +29,9 @@ class RequestRouter:
self.__cache = cache
self.__f = f
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
m = RequestRouter.__urlMatcher.findall(url)
......@@ -49,19 +53,23 @@ class RequestRouter:
@property
def cache(self):
"""Indicates if this URL should be cached or not"""
'''Indicates if this URL should be cached or not'''
return self.__cache
@property
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
def addValidator(self,fieldName,validator):
'''Adds additional field-specific formencode validators'''
self.__validators[fieldName] = validator
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)
or dict with arg/values for dynamic URLs
"""
'''
g = self.__matcher.search(url)
if g != None:
args = g.groupdict()
......@@ -76,14 +84,17 @@ class RequestRouter:
return None
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)
class CachedUrl():
"""
'''
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
"""
'''
def __init__(self,router,args):
self.__router = router
self.__args = args
......@@ -110,6 +121,7 @@ class CorePost(Resource):
self.__urls = defaultdict(dict)
self.__cachedUrls = defaultdict(dict)
self.__methods = {}
self.__routers = {}
self.__path = path
self.__schema = schema
......@@ -117,24 +129,34 @@ class CorePost(Resource):
def path(self):
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 not isinstance(methods,(list,tuple)):
methods = (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.__routers[f] = rq # needed so that we can lookup the router for a specific function
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 """
def wrap(f):
self.__registerFunction(f, url, methods, accepts, produces,cache, schema)
def wrap(f,*args,**kwargs):
self.__registerFunction(f, url, methods, accepts, produces,cache)
return f
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):
""" Handles all GET requests """
return self.__renderUrl(request)
......@@ -187,27 +209,31 @@ class CorePost(Resource):
# if POST/PUT, check if we need to automatically parse JSON
# TODO
#validate input against formencode schema if defined
self.__validateArguments(urlrouter, request, allargs)
#handle Deferreds natively
val = urlrouter.call(request,**allargs)
if isinstance(val,defer.Deferred):
# we assume the method will call request.finish()
return NOT_DONE_YET
else:
return val
try:
val = urlrouter.call(request,**allargs)
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)
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:
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):
"""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