Browse Source

add aiohttp doc generation support

Bastien Sevajol 5 years ago
parent
commit
6bad337bc7
2 changed files with 132 additions and 8 deletions
  1. 49 8
      hapic/ext/aiohttp/context.py
  2. 83 0
      tests/ext/unit/test_aiohttp.py

+ 49 - 8
hapic/ext/aiohttp/context.py View File

@@ -1,9 +1,8 @@
1 1
 # coding: utf-8
2
-import asyncio
3 2
 import json
3
+import re
4 4
 import typing
5 5
 from http import HTTPStatus
6
-from json import JSONDecodeError
7 6
 
8 7
 from aiohttp.web_request import Request
9 8
 from aiohttp.web_response import Response
@@ -12,13 +11,22 @@ from multidict import MultiDict
12 11
 from hapic.context import BaseContext
13 12
 from hapic.context import RouteRepresentation
14 13
 from hapic.decorator import DecoratedController
15
-from hapic.error import ErrorBuilderInterface, DefaultErrorBuilder
16
-from hapic.exception import WorkflowException, OutputValidationException
14
+from hapic.decorator import DECORATION_ATTRIBUTE_NAME
15
+from hapic.error import ErrorBuilderInterface
16
+from hapic.error import DefaultErrorBuilder
17
+from hapic.exception import WorkflowException
18
+from hapic.exception import OutputValidationException
19
+from hapic.exception import NoRoutesException
20
+from hapic.exception import RouteNotFound
17 21
 from hapic.processor import ProcessValidationError
18 22
 from hapic.processor import RequestParameters
19 23
 from aiohttp import web
20 24
 
21 25
 
26
+# Bottle regular expression to locate url parameters
27
+AIOHTTP_RE_PATH_URL = re.compile(r'{([^:<>]+)(?::[^<>]+)?}')
28
+
29
+
22 30
 class AiohttpRequestParameters(object):
23 31
     def __init__(
24 32
         self,
@@ -137,15 +145,48 @@ class AiohttpContext(BaseContext):
137 145
         self,
138 146
         decorated_controller: DecoratedController,
139 147
     ) -> RouteRepresentation:
140
-        # TODO BS 2018-07-15: to do
141
-        raise NotImplementedError('todo')
148
+        if not len(self.app.router.routes()):
149
+            raise NoRoutesException('There is no routes in your aiohttp app')
150
+
151
+        reference = decorated_controller.reference
152
+
153
+        for route in self.app.router.routes():
154
+            route_token = getattr(
155
+                route.handler,
156
+                DECORATION_ATTRIBUTE_NAME,
157
+                None,
158
+            )
159
+
160
+            match_with_wrapper = route.handler == reference.wrapper
161
+            match_with_wrapped = route.handler == reference.wrapped
162
+            match_with_token = route_token == reference.token
163
+
164
+            # TODO BS 2018-07-27: token is set in HEAD view to, must solve this
165
+            # case
166
+            if not match_with_wrapper \
167
+                    and not match_with_wrapped \
168
+                    and match_with_token \
169
+                    and route.method.lower() == 'head':
170
+                continue
171
+
172
+            if match_with_wrapper or match_with_wrapped or match_with_token:
173
+                return RouteRepresentation(
174
+                    rule=self.get_swagger_path(route.resource.canonical),
175
+                    method=route.method.lower(),
176
+                    original_route_object=route,
177
+                )
178
+        # TODO BS 20171010: Raise exception or print error ? see #10
179
+        raise RouteNotFound(
180
+            'Decorated route "{}" was not found in aiohttp routes'.format(
181
+                decorated_controller.name,
182
+            )
183
+        )
142 184
 
143 185
     def get_swagger_path(
144 186
         self,
145 187
         contextualised_rule: str,
146 188
     ) -> str:
147
-        # TODO BS 2018-07-15: to do
148
-        raise NotImplementedError('todo')
189
+        return AIOHTTP_RE_PATH_URL.sub(r'{\1}', contextualised_rule)
149 190
 
150 191
     def by_pass_output_wrapping(
151 192
         self,

+ 83 - 0
tests/ext/unit/test_aiohttp.py View File

@@ -229,3 +229,86 @@ class TestAiohttpExt(object):
229 229
         assert b'{"name": "Hello, franck"}\n' == line
230 230
 
231 231
         # TODO BS 2018-07-26: How to ensure we are at end of response ?
232
+
233
+    def test_unit__generate_doc__ok__nominal_case(
234
+        self,
235
+        aiohttp_client,
236
+        loop,
237
+    ):
238
+        hapic = Hapic(async_=True)
239
+
240
+        class InputPathSchema(marshmallow.Schema):
241
+            username = marshmallow.fields.String(required=True)
242
+
243
+        class InputQuerySchema(marshmallow.Schema):
244
+            show_deleted = marshmallow.fields.Boolean(required=False)
245
+
246
+        class UserSchema(marshmallow.Schema):
247
+            name = marshmallow.fields.String(required=True)
248
+
249
+        @hapic.with_api_doc()
250
+        @hapic.input_path(InputPathSchema())
251
+        @hapic.input_query(InputQuerySchema())
252
+        @hapic.output_body(UserSchema())
253
+        async def get_user(request, hapic_data):
254
+            pass
255
+
256
+        @hapic.with_api_doc()
257
+        @hapic.input_path(InputPathSchema())
258
+        @hapic.output_body(UserSchema())
259
+        async def post_user(request, hapic_data):
260
+            pass
261
+
262
+        app = web.Application(debug=True)
263
+        app.router.add_get('/{username}', get_user)
264
+        app.router.add_post('/{username}', post_user)
265
+        hapic.set_context(AiohttpContext(app))
266
+
267
+        doc = hapic.generate_doc('aiohttp', 'testing')
268
+        assert 'UserSchema' in doc.get('definitions')
269
+        assert {
270
+                   'name': {'type': 'string'}
271
+               } == doc['definitions']['UserSchema'].get('properties')
272
+        assert '/{username}' in doc.get('paths')
273
+        assert 'get' in doc['paths']['/{username}']
274
+        assert 'post' in doc['paths']['/{username}']
275
+
276
+        assert [
277
+            {
278
+                'name': 'username',
279
+                'in': 'path',
280
+                'required': True,
281
+                'type': 'string',
282
+            },
283
+            {
284
+                'name': 'show_deleted',
285
+                'in': 'query',
286
+                'required': False,
287
+                'type': 'boolean',
288
+            }
289
+        ] == doc['paths']['/{username}']['get']['parameters']
290
+        assert {
291
+            200: {
292
+                'schema': {
293
+                    '$ref': '#/definitions/UserSchema',
294
+                },
295
+                'description': '200',
296
+            }
297
+        } == doc['paths']['/{username}']['get']['responses']
298
+
299
+        assert [
300
+                   {
301
+                       'name': 'username',
302
+                       'in': 'path',
303
+                       'required': True,
304
+                       'type': 'string',
305
+                   }
306
+               ] == doc['paths']['/{username}']['post']['parameters']
307
+        assert {
308
+                   200: {
309
+                       'schema': {
310
+                           '$ref': '#/definitions/UserSchema',
311
+                       },
312
+                       'description': '200',
313
+                   }
314
+               } == doc['paths']['/{username}']['get']['responses']