From ada6083457391048490a77992f4b22de27aee73b Mon Sep 17 00:00:00 2001 From: Amit Nabarro Date: Sat, 18 Nov 2017 19:38:45 +0200 Subject: [PATCH] refactor resource class 1. move hypermedia from format to dispatch method 2. add wrap_response method as dispatch argument to support response processing before data formatting --- docs/source/resources.rst | 115 ++++++++++++++++++++++++++++++++++- tbone/resources/resources.py | 36 +++++------ 2 files changed, 132 insertions(+), 19 deletions(-) diff --git a/docs/source/resources.rst b/docs/source/resources.rst index 6be0c6e..e6001ff 100644 --- a/docs/source/resources.rst +++ b/docs/source/resources.rst @@ -142,7 +142,52 @@ Adding additional links to the resource is done by overriding ``add_hypermedia`` Nested Resources ------------------ -Nested resources ... +Nested resources is a technique to extend a resource's endpoints beyond basic CRUD. Every resource automatically exposes the HTTP verbs (GET, POST, PUT, PATCH, DELETE) with their respective methods, adhereing to REST principles. +However, it is sometimes neccesary to extend a resource's functionality by implementing additional endpoints. +These can be described by two categories: + 1. Resources which expose nested resources classes + 2. Resources which expose additional unrest endpoints serving specific functionality. + +Lets look at some examples:: + + # model representing a user's blog comment. Internal + class Comment(Model): + user = StringField() + content = StringField() + + # model representing a single blog post, includes a list of comments + class Blog(Model): + title = StringField() + content = StringField() + comments = ListField(ModelField(Comment)) + + + class CommentResource(ModelResource): + class Meta: + object_class = Comment + + + class BlogResource(ModelResource): + class Meta: + object_class = Blog + + @classmethod + def nested_routes(cls, base_url): + return [ + Route( + path=base_url + '%s/comments/add/' % (cls.route_param('pk')), + handler=cls.add_comment, + methods=cls.route_methods(), + name='blog_add_comment') + ] + + @classmethod + async def add_comment(cls, request, **kwargs): + + + + + MongoDB Resources @@ -261,6 +306,71 @@ This will result in a usage like so:: /api/books/?fts=history +Hooking up to application's router +------------------------------------ +Once a resource has been implemented, it needs to be hooked up to the application's router. +With any web application such as Sanic or AioHttp, adding handlers to the application involves matching a uri to a specific handler method. The ``Resource`` class implements two methods ``to_list`` and ``to_detail`` which create list handlers and detail handlers respectively, for the application router, like so:: + + app.add_route('GET', '/books', BookResource.as_list()) + app.add_route('GET', '/books/{id}', BookResource.as_detail()) + +The syntax varies a little, depending on the web server used. + +Sanic Example +~~~~~~~~~~~~~~ + +:: + + from sanic import Sanic + from tbone.resources import Resource + from tbone.resources.sanic import SanicResource + + + class TestResource(SanicResource, Resource): + async def list(self, **kwargs): + return { + 'meta': {}, + 'objects': [ + {'text': 'hello world'} + ] + } + + app = Sanic() + app.add_route(methods=['GET'], uri='/', handler=TestResource.as_list()) + + if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) + + +AioHttp Example +~~~~~~~~~~~~~~~~ + +:: + + from aiohttp import web + from tbone.resources import Resource + from tbone.resources.aiohttp import AioHttpResource + + + class TestResource(AioHttpResource, Resource): + async def list(self, **kwargs): + return { + 'meta': {}, + 'objects': [ + {'text': 'hello world'} + ] + } + + app = web.Application() + app.router.add_get('/', TestResource.as_list()) + + if __name__ == "__main__": + web.run_app(app, host='127.0.0.1', port=8000) + + +The examples above demonstrate how to manually add resources to the application router. This can become tedious when the app has multiple resources which expose list and detail endpoints as well as some nested resources. +An alternative way is to use a ``Router`` , described below. + Routers ---------- @@ -329,3 +439,6 @@ With ``Sanic`` it looks like this:: + + + diff --git a/tbone/resources/resources.py b/tbone/resources/resources.py index f4b8ed5..2a383e2 100644 --- a/tbone/resources/resources.py +++ b/tbone/resources/resources.py @@ -235,7 +235,7 @@ def is_method_allowed(self, endpoint, method): return True return False - async def dispatch(self, endpoint, *args, **kwargs): + async def dispatch(self, endpoint, wrap_response=None, *args, **kwargs): ''' This method handles the actual request to the resource. It performs all the neccesary checks and then executes the relevant member method which is mapped to the method name. @@ -271,7 +271,18 @@ async def dispatch(self, endpoint, *args, **kwargs): view_method = getattr(self, self.http_methods[endpoint][method]) # call request method data = await view_method(*args, **kwargs) - # add request_uri + # add hypermedia to the response + if self._meta.hypermedia is True: + if endpoint == 'list': + for item in data['objects']: + self.add_hypermedia(item) + elif endpoint == 'detail': + self.add_hypermedia(data) + + # wrap response data + if callable(wrap_response): + data = wrap_response(data) + # format the response object formatted = self.format(method, endpoint, data) except Exception as ex: return self.dispatch_error(ex) @@ -299,7 +310,7 @@ def build_response(cls, data, status=200): Given some data, generates an HTTP response. If you're integrating with a new web framework, other than sanic or aiohttp, you **MUST** override this method within your subclass. - + :param data: The body of the response to send :type data: @@ -333,6 +344,8 @@ def get_resource_uri(self): def parse(self, method, endpoint, body): ''' calls parse on list or detail ''' + if isinstance(body, dict): # request body was already parsed + return body if endpoint == 'list': return self.parse_list(body) @@ -375,26 +388,12 @@ def format(self, method, endpoint, data): def format_list(self, data): if data is None: return '' - if self._meta.hypermedia is True: - # add resource uri - for item in data['objects']: - self.add_hypermedia(item) - return self._meta.formatter.format(data) def format_detail(self, data): if data is None: return '' - if self._meta.hypermedia is True: - self.add_hypermedia(data) - - return self._meta.formatter.format(self.get_resource_data(data)) - - def get_resource_data(self, data): - resource_data = {} - for k, v in data.items(): - resource_data[k] = v - return resource_data + return self._meta.formatter.format(data) @classmethod def connect_signal_receivers(cls): @@ -441,6 +440,7 @@ class ModelResource(Resource): ''' A specialized resource class for using data models. Requires further implementation for data persistency ''' + def __init__(self, *args, **kwargs): super(ModelResource, self).__init__(*args, **kwargs) # verify object class has a declared primary key