Browse Source

add aiohttp doc generation support

Bastien Sevajol 6 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
 # coding: utf-8
1
 # coding: utf-8
2
-import asyncio
3
 import json
2
 import json
3
+import re
4
 import typing
4
 import typing
5
 from http import HTTPStatus
5
 from http import HTTPStatus
6
-from json import JSONDecodeError
7
 
6
 
8
 from aiohttp.web_request import Request
7
 from aiohttp.web_request import Request
9
 from aiohttp.web_response import Response
8
 from aiohttp.web_response import Response
12
 from hapic.context import BaseContext
11
 from hapic.context import BaseContext
13
 from hapic.context import RouteRepresentation
12
 from hapic.context import RouteRepresentation
14
 from hapic.decorator import DecoratedController
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
 from hapic.processor import ProcessValidationError
21
 from hapic.processor import ProcessValidationError
18
 from hapic.processor import RequestParameters
22
 from hapic.processor import RequestParameters
19
 from aiohttp import web
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
 class AiohttpRequestParameters(object):
30
 class AiohttpRequestParameters(object):
23
     def __init__(
31
     def __init__(
24
         self,
32
         self,
137
         self,
145
         self,
138
         decorated_controller: DecoratedController,
146
         decorated_controller: DecoratedController,
139
     ) -> RouteRepresentation:
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
     def get_swagger_path(
185
     def get_swagger_path(
144
         self,
186
         self,
145
         contextualised_rule: str,
187
         contextualised_rule: str,
146
     ) -> str:
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
     def by_pass_output_wrapping(
191
     def by_pass_output_wrapping(
151
         self,
192
         self,

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

229
         assert b'{"name": "Hello, franck"}\n' == line
229
         assert b'{"name": "Hello, franck"}\n' == line
230
 
230
 
231
         # TODO BS 2018-07-26: How to ensure we are at end of response ?
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']