""" API_magic Author: Flowgram.com ======== Purpose ======== Defines a decorator ``API_magic'' which simplifies input-checking API views. ======== Parameters to API_magic ======== API_magic takes four parameters 1) The HTTP method (GET or POST) that the API call expects 2) A list ``required_args'' of the names of required arguments. These are then passed to the function as positional arguments, possibly in an altered form. See "Parameter Check and Transformation" below. 3) A dictionary ``permissions'' of permissions the requesting user must possess in order to execute the call on the given parameters. See "Permissions" below. 4) A boolean ``login_required'' indicating whether the requesting user must be authenticated. ======== Return value of decorated function ======== If there is an error in the method, arguments, transformations, or permissions checking, API_magic will return a response based on the error. If the API call returns a value (other than None), that value will be returned. Finally, the call will return a default 'ok' response. ======== Requirements ======== The following variables must be defined: - ``response'': A function taking two arguments: a response code and an optional body message, returning an HttpResponse object. - ``permission_functions'': A dictionary mapping permission names to permission-checking functions. A permission-checking function takes two arguments: a User objects and some other object, and returns True if the user has the permission on that object. - ``permission_responses'': A dictionary mapping permission names to response functions. A response function takes an error message and returns an HttpResponse object. - ``transformations'': A dictionary mapping request argument names to (type_name, converter_function) tuples. A converter_function is a function that takes the value of the request argument (a string) and returns an object of type type_name. ======== Example usage ======== # code before using API_magic def some_api_function(request): if request.method != 'POST': return response('method-post') if not 'foobar_id' in request.POST.keys(): return response('input-missing', 'foobar_id is a required POST arg') try: foobar = Foobar.objects.get(id=request.POST['foobar_id']) except Foobar.DoesNotExist: msg = "foobar_id %s could not be converted to Foobar object" % foobar_id return response('input-invalid', msg) if not permissions.can_edit(foobar, request.user): msg = "%s does not have permission to edit that foobar_id" % request.user return response('permission-edit', msg) if not 'title' in request.POST.keys(): return response('input-missing', 'title is a required POST arg') ...do work here... return response('Done') # code after using API_magic @API_magic('POST', ['foobar_id', 'title'], {'foobar_id': 'edit'}) def some_api_function(request, foobar, title): ...do work here... return response('Done') ======== Parameter Check and Transformation ======== A function decorated with API_magic validates the presence of required arguments named in the second parameter. For the example just above, it would make sure that there are arguments named 'foobar_id' and 'title' in request.POST, returning an API error response otherwise. Of course, optional parameters can still be used, you just don't include them in the required arguments list. Next, transformations are applied based on the argument's name. Transformations generally convert the argument from strings to another type. This is probably the most magical thing going on. There is a dictionary in this module called 'transformations' which maps arg-names to the transformation to be applied. Any explicitly required function argument with a name among the transformations will be transformed before it reaches the main function code. The keys in the 'transformations' dictionary are (typename, convert function) pairs. The typename is a string to be used in the API error message that is returned if the conversion fails. For the example above, the 'foobar_id' argument would be transformed to a Foobar object before it is passed to the function. ======== Permissions ======== The third argument to API_magic is an optional dictionary of permissions to be checked. The keys are strings that match required-argument names, and the values are strings that are keys in both the permission_functions and permission_responses dictionaries locally defined. The values of the permission_functions dictionary are permission-checking functions which return a boolean value. The argument is transformed *before* it is passed to the permission checker -- for the example above, the 'edit' permission checker is passed a Foobar object, not a foobar_id (string). If the permission check fails (returns false), the decorated function will return an error response defined by the permission_responses function for the given permission -- these functions take a string error messsage as an argument. """ from functools import wraps from django.contrib.auth.models import User def response(code, msg=''): """Simple example response generator""" from django.utils import simplejson r = {'code': code, 'succeeded': 1 if code=='ok' else 0, 'body': msg } return HttpResponse(simplejson.dumps(r), mimetype='application/json') # you need to make your own functions here, obviously: from core import permissions permission_functions = { 'view' : permissions.can_view, 'edit' : permissions.can_edit, } permission_responses = { 'view' : lambda x: response('permission-view', x), 'edit' : lambda x: response('permission-edit', x) } def fetch_foobar(foobarid): from core.models import Foobar return Foobar.objects.get(id=foobarid) transformations = { # arg name # Type name # converter function 'foobar_id' : ('Foobar', fetch_foobar), 'time' : ('integer', int), } def API_magic(method='POST', required_args=[], permissions={}, login_required=False): def check_args(func): # First, validate arguments *to API_magic* and raise an exception if they are bad. # This happens when the code is first loaded by the server. # # validate the method: if method not in ['GET', 'POST']: # add more here if you want err_msg = "Invalid method %s passed to API_magic by %s" % (method, func.__name__) raise ValueError(err_msg) # validate the permissions: for arg_name, perm_name in permissions.items(): if arg_name not in required_args: err_msg = "Permission on unknown arg %s passed to API_magic by %s" % (arg_name, func.__name__) raise ValueError(err_msg) if perm_name not in permission_functions.keys(): err_msg = "Permission %s with unknown checker function passed to API_magic by %s"\ % (perm_name, func.__name__) raise ValueError(err_msg) if perm_name not in permission_responses.keys(): err_msg = "Permission %s with unknown response passed to API_magic by %s" % (perm_name, func.__name__) raise ValueError(err_msg) # Next, create an input-validating version of the decorated function: @wraps(func) def new_f(request, *args, **kwargs): # Validate authentication if login_required: if not request.user.is_authenticated(): return response('login') # Validate call method and get the arguments if method == 'GET': if request.method != 'GET': return response('method-get') arg_box = request.GET else: # method == 'POST': if request.method != 'POST': return response('method-post') arg_box = request.POST # For each required argument: for name in required_args: # Make sure it's present if not name in arg_box.keys(): return response('input-missing', "%s is a required %s argument" % (name, method)) value = arg_box[name] # Apply any transformation if name in transformations.keys(): type_name, converter = transformations[name] try: value = converter(value) except: err_msg = "%s %s could not be converted to %s object" % (name, value, type_name) return response('input-invalid', err_msg) # Check permissions on it if name in permissions.keys(): permission_name = permissions[name] perm_checker = permission_functions[permission_name] perm_responder = permission_responses[permission_name] if not perm_checker(request.user, value): err_msg = "%s does not have permission to %s that %s" % (request.user, permission_name, name) return perm_responder(err_msg) # Add the (possibly transformed) value to the fixed function args args += (value,) # Call function with new arguments and return the function's return, or a default 'ok' r = func(request, *args, **kwargs) return r if r else response('ok') return new_f return check_args