Browse Source

Add swagger ui doc visualisation

Bastien Sevajol 6 years ago
parent
commit
64819802ee

+ 1 - 0
example/example_a_flask.py View File

90
 controllers.bind(app)
90
 controllers.bind(app)
91
 
91
 
92
 hapic.set_context(FlaskContext(app))
92
 hapic.set_context(FlaskContext(app))
93
+hapic.add_documentation_view('/api-doc', 'DOC', 'Generated doc')
93
 print(json.dumps(hapic.generate_doc()))
94
 print(json.dumps(hapic.generate_doc()))
94
 app.run(host='localhost', port=8080, debug=True)
95
 app.run(host='localhost', port=8080, debug=True)

+ 1 - 0
hapic/__init__.py View File

16
 output_file = _hapic_default.output_file
16
 output_file = _hapic_default.output_file
17
 generate_doc = _hapic_default.generate_doc
17
 generate_doc = _hapic_default.generate_doc
18
 set_context = _hapic_default.set_context
18
 set_context = _hapic_default.set_context
19
+add_documentation_view = _hapic_default.add_documentation_view
19
 handle_exception = _hapic_default.handle_exception
20
 handle_exception = _hapic_default.handle_exception

+ 28 - 1
hapic/context.py View File

34
     def get_response(
34
     def get_response(
35
         self,
35
         self,
36
         # TODO BS 20171228: rename into response_content
36
         # TODO BS 20171228: rename into response_content
37
-        response: dict,
37
+        response: str,
38
         http_code: int,
38
         http_code: int,
39
+        mimetype: str='application/json',
39
     ) -> typing.Any:
40
     ) -> typing.Any:
40
         raise NotImplementedError()
41
         raise NotImplementedError()
41
 
42
 
79
         """
80
         """
80
         raise NotImplementedError()
81
         raise NotImplementedError()
81
 
82
 
83
+    def add_view(
84
+        self,
85
+        route: str,
86
+        http_method: str,
87
+        view_func: typing.Callable[..., typing.Any],
88
+    ) -> None:
89
+        """
90
+        This method must permit to add a view in current context
91
+        :param route: The route depending of framework format, ex "/foo"
92
+        :param http_method: HTTP method like GET, POST, etc ...
93
+        :param view_func: The view callable
94
+        """
95
+        raise NotImplementedError()
96
+
97
+    def serve_directory(
98
+        self,
99
+        route_prefix: str,
100
+        directory_path: str,
101
+    ) -> None:
102
+        """
103
+        Configure a path to serve a directory content
104
+        :param route_prefix: The base url for serve the directory, eg /static
105
+        :param directory_path: The file system path
106
+        """
107
+        raise NotImplementedError()
108
+
82
 
109
 
83
 class BaseContext(ContextInterface):
110
 class BaseContext(ContextInterface):
84
     def get_default_error_builder(self) -> ErrorBuilderInterface:
111
     def get_default_error_builder(self) -> ErrorBuilderInterface:

+ 4 - 2
hapic/decorator.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import json
3
+
2
 import functools
4
 import functools
3
 import typing
5
 import typing
4
 try:  # Python 3.5+
6
 try:  # Python 3.5+
221
 
223
 
222
             processed_response = self.processor.process(response)
224
             processed_response = self.processor.process(response)
223
             prepared_response = self.context.get_response(
225
             prepared_response = self.context.get_response(
224
-                processed_response,
226
+                json.dumps(processed_response),
225
                 self.default_http_code,
227
                 self.default_http_code,
226
             )
228
             )
227
             return prepared_response
229
             return prepared_response
432
                 )
434
                 )
433
 
435
 
434
             error_response = self.context.get_response(
436
             error_response = self.context.get_response(
435
-                response_content,
437
+                json.dumps(response_content),
436
                 self.http_code,
438
                 self.http_code,
437
             )
439
             )
438
             return error_response
440
             return error_response

+ 27 - 0
hapic/doc.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import json
3
+
2
 import typing
4
 import typing
5
+import yaml
3
 
6
 
4
 from apispec import APISpec
7
 from apispec import APISpec
5
 from apispec import Path
8
 from apispec import Path
188
 
191
 
189
         return spec.to_dict()
192
         return spec.to_dict()
190
 
193
 
194
+    def save_in_file(
195
+        self,
196
+        doc_file_path: str,
197
+        controllers: typing.List[DecoratedController],
198
+        context: ContextInterface,
199
+        title: str='',
200
+        description: str='',
201
+    ) -> None:
202
+        # generate this file
203
+        dict_doc = self.get_doc(
204
+            controllers=controllers,
205
+            context=context,
206
+            title=title,
207
+            description=description,
208
+        )
209
+        json_doc = json.dumps(dict_doc)
210
+
211
+        # We dump then load with json to use real scalar dict.
212
+        # If not, yaml dump dict-like objects
213
+        clean_dict_doc = json.loads(json_doc)
214
+        yaml_doc = yaml.dump(clean_dict_doc, default_flow_style=False)
215
+        with open(doc_file_path, 'w+') as doc_file:
216
+            doc_file.write(yaml_doc)
217
+
191
 
218
 
192
 # TODO BS 20171109: Must take care of already existing definition names
219
 # TODO BS 20171109: Must take care of already existing definition names
193
 def generate_schema_name(schema):
220
 def generate_schema_name(schema):

+ 4 - 3
hapic/ext/bottle/context.py View File

56
 
56
 
57
     def get_response(
57
     def get_response(
58
         self,
58
         self,
59
-        response: dict,
59
+        response: str,
60
         http_code: int,
60
         http_code: int,
61
+        mimetype: str='application/json',
61
     ) -> bottle.HTTPResponse:
62
     ) -> bottle.HTTPResponse:
62
         return bottle.HTTPResponse(
63
         return bottle.HTTPResponse(
63
-            body=json.dumps(response),
64
+            body=response,
64
             headers=[
65
             headers=[
65
-                ('Content-Type', 'application/json'),
66
+                ('Content-Type', mimetype),
66
             ],
67
             ],
67
             status=http_code,
68
             status=http_code,
68
         )
69
         )

+ 36 - 3
hapic/ext/flask/context.py View File

18
 from hapic.error import DefaultErrorBuilder
18
 from hapic.error import DefaultErrorBuilder
19
 from hapic.error import ErrorBuilderInterface
19
 from hapic.error import ErrorBuilderInterface
20
 from flask import Flask
20
 from flask import Flask
21
+from flask import send_from_directory
21
 
22
 
22
 if typing.TYPE_CHECKING:
23
 if typing.TYPE_CHECKING:
23
     from flask import Response
24
     from flask import Response
49
 
50
 
50
     def get_response(
51
     def get_response(
51
         self,
52
         self,
52
-        response: dict,
53
+        response: str,
53
         http_code: int,
54
         http_code: int,
55
+        mimetype: str='application/json',
54
     ) -> 'Response':
56
     ) -> 'Response':
55
         from flask import Response
57
         from flask import Response
56
         return Response(
58
         return Response(
57
-            response=json.dumps(response),
58
-            mimetype='application/json',
59
+            response=response,
60
+            mimetype=mimetype,
59
             status=http_code,
61
             status=http_code,
60
         )
62
         )
61
 
63
 
123
     def by_pass_output_wrapping(self, response: typing.Any) -> bool:
125
     def by_pass_output_wrapping(self, response: typing.Any) -> bool:
124
         from flask import Response
126
         from flask import Response
125
         return isinstance(response, Response)
127
         return isinstance(response, Response)
128
+
129
+    def add_view(
130
+        self,
131
+        route: str,
132
+        http_method: str,
133
+        view_func: typing.Callable[..., typing.Any],
134
+    ) -> None:
135
+        self.app.add_url_rule(
136
+            rule=route,
137
+            view_func=view_func,
138
+        )
139
+
140
+    def serve_directory(
141
+        self,
142
+        route_prefix: str,
143
+        directory_path: str,
144
+    ) -> None:
145
+        if not route_prefix.endswith('/'):
146
+            route_prefix = '{}/'.format(route_prefix)
147
+
148
+        @self.app.route(
149
+            route_prefix,
150
+            defaults={
151
+                'path': 'index.html',
152
+            }
153
+        )
154
+        @self.app.route(
155
+            '{}<path:path>'.format(route_prefix),
156
+        )
157
+        def api_doc(path):
158
+            return send_from_directory(directory_path, path)

+ 4 - 3
hapic/ext/pyramid/context.py View File

57
 
57
 
58
     def get_response(
58
     def get_response(
59
         self,
59
         self,
60
-        response: dict,
60
+        response: str,
61
         http_code: int,
61
         http_code: int,
62
+        mimetype: str='application/json',
62
     ) -> 'Response':
63
     ) -> 'Response':
63
         from pyramid.response import Response
64
         from pyramid.response import Response
64
         return Response(
65
         return Response(
65
-            body=json.dumps(response),
66
+            body=response,
66
             headers=[
67
             headers=[
67
-                ('Content-Type', 'application/json'),
68
+                ('Content-Type', mimetype),
68
             ],
69
             ],
69
             status=http_code,
70
             status=http_code,
70
         )
71
         )

+ 71 - 0
hapic/hapic.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import os
2
 import typing
3
 import typing
3
 import uuid
4
 import uuid
4
 import functools
5
 import functools
376
             title=title,
377
             title=title,
377
             description=description,
378
             description=description,
378
         )
379
         )
380
+
381
+    def save_doc_in_file(
382
+        self,
383
+        file_path: str,
384
+        title: str='',
385
+        description: str='',
386
+    ) -> None:
387
+        """
388
+        See hapic.doc.DocGenerator#get_doc docstring
389
+        :param file_path: The file path to write doc in YAML format
390
+        :param title: Title of generated doc
391
+        :param description: Description of generated doc
392
+        """
393
+        self.doc_generator.save_in_file(
394
+            file_path,
395
+            controllers=self._controllers,
396
+            context=self.context,
397
+            title=title,
398
+            description=description,
399
+        )
400
+
401
+    def add_documentation_view(
402
+        self,
403
+        route: str,
404
+        title: str='',
405
+        description: str='',
406
+    ) -> None:
407
+        # Ensure "/" at end of route, else web browser will not consider it as
408
+        # a path
409
+        if not route.endswith('/'):
410
+            route = '{}/'.format(route)
411
+
412
+        # Add swagger directory as served static dir
413
+        swaggerui_path = os.path.join(
414
+            os.path.dirname(os.path.abspath(__file__)),
415
+            'static',
416
+            'swaggerui',
417
+        )
418
+        self.context.serve_directory(
419
+            route,
420
+            swaggerui_path,
421
+        )
422
+
423
+        # Generate documentation file
424
+        doc_page_path = os.path.join(swaggerui_path, 'spec.yml')
425
+        self.save_doc_in_file(doc_page_path)
426
+
427
+        # Prepare views html content
428
+        doc_index_path = os.path.join(swaggerui_path, 'index.html')
429
+        with open(doc_index_path, 'r') as doc_page:
430
+            doc_page_content = doc_page.read()
431
+        doc_page_content = doc_page_content.replace(
432
+            '{{ spec_uri }}',
433
+            'spec.yml',
434
+        )
435
+
436
+        # Declare the swaggerui view
437
+        def api_doc_view():
438
+            return self.context.get_response(
439
+                doc_page_content,
440
+                http_code=HTTPStatus.OK,
441
+                mimetype='text/html',
442
+            )
443
+
444
+        # Add a view to generate the html index page of swaggerui
445
+        self.context.add_view(
446
+            route=route,
447
+            http_method='GET',
448
+            view_func=api_doc_view,
449
+        )

BIN
hapic/static/swaggerui/favicon-16x16.png View File


BIN
hapic/static/swaggerui/favicon-32x32.png View File


+ 95 - 0
hapic/static/swaggerui/index.html View File

1
+<!-- HTML for static distribution bundle build -->
2
+<!DOCTYPE html>
3
+<html lang="en">
4
+<head>
5
+  <meta charset="UTF-8">
6
+  <title>Swagger UI</title>
7
+  <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
8
+  <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
9
+  <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
10
+  <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
11
+  <style>
12
+    html
13
+    {
14
+      box-sizing: border-box;
15
+      overflow: -moz-scrollbars-vertical;
16
+      overflow-y: scroll;
17
+    }
18
+    *,
19
+    *:before,
20
+    *:after
21
+    {
22
+      box-sizing: inherit;
23
+    }
24
+
25
+    body {
26
+      margin:0;
27
+      background: #fafafa;
28
+    }
29
+  </style>
30
+</head>
31
+
32
+<body>
33
+
34
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
35
+  <defs>
36
+    <symbol viewBox="0 0 20 20" id="unlocked">
37
+          <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
38
+    </symbol>
39
+
40
+    <symbol viewBox="0 0 20 20" id="locked">
41
+      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
42
+    </symbol>
43
+
44
+    <symbol viewBox="0 0 20 20" id="close">
45
+      <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
46
+    </symbol>
47
+
48
+    <symbol viewBox="0 0 20 20" id="large-arrow">
49
+      <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
50
+    </symbol>
51
+
52
+    <symbol viewBox="0 0 20 20" id="large-arrow-down">
53
+      <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
54
+    </symbol>
55
+
56
+
57
+    <symbol viewBox="0 0 24 24" id="jump-to">
58
+      <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
59
+    </symbol>
60
+
61
+    <symbol viewBox="0 0 24 24" id="expand">
62
+      <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
63
+    </symbol>
64
+
65
+  </defs>
66
+</svg>
67
+
68
+<div id="swagger-ui"></div>
69
+
70
+<script src="./swagger-ui-bundle.js"> </script>
71
+<script src="./swagger-ui-standalone-preset.js"> </script>
72
+<script>
73
+window.onload = function() {
74
+  
75
+  // Build a system
76
+  const ui = SwaggerUIBundle({
77
+    url: "{{ spec_uri }}",
78
+    dom_id: '#swagger-ui',
79
+    deepLinking: true,
80
+    presets: [
81
+      SwaggerUIBundle.presets.apis,
82
+      SwaggerUIStandalonePreset
83
+    ],
84
+    plugins: [
85
+      SwaggerUIBundle.plugins.DownloadUrl
86
+    ],
87
+    layout: "StandaloneLayout"
88
+  })
89
+
90
+  window.ui = ui
91
+}
92
+</script>
93
+</body>
94
+
95
+</html>

+ 94 - 0
hapic/static/swaggerui/spec.yml View File

1
+definitions:
2
+  DefaultErrorBuilder:
3
+    properties:
4
+      code:
5
+        type: string
6
+        x-nullable: true
7
+      details:
8
+        type: object
9
+      message:
10
+        type: string
11
+    required:
12
+    - message
13
+    type: object
14
+  HelloJsonSchema:
15
+    properties:
16
+      color:
17
+        minLength: 3
18
+        type: string
19
+    required:
20
+    - color
21
+    type: object
22
+  HelloResponseSchema:
23
+    properties:
24
+      color:
25
+        type: string
26
+      name:
27
+        type: string
28
+      sentence:
29
+        type: string
30
+    required:
31
+    - name
32
+    - sentence
33
+    type: object
34
+info:
35
+  description: ''
36
+  title: ''
37
+  version: 1.0.0
38
+parameters: {}
39
+paths:
40
+  /hello/{name}:
41
+    get:
42
+      description: "my endpoint hello\n        ---\n        get:\n            description:\
43
+        \ my description\n            parameters:\n                - in: \"path\"\n\
44
+        \                  description: \"hello\"\n                  name: \"name\"\
45
+        \n                  type: \"string\"\n            responses:\n           \
46
+        \     200:\n                    description: A pet to be returned\n      \
47
+        \              schema: HelloResponseSchema"
48
+      parameters:
49
+      - in: path
50
+        name: name
51
+        required: true
52
+        type: string
53
+      - in: query
54
+        name: alive
55
+        required: false
56
+        type: boolean
57
+      responses:
58
+        '200':
59
+          description: '200'
60
+          schema:
61
+            $ref: '#/definitions/HelloResponseSchema'
62
+        '400':
63
+          description: '400'
64
+          schema:
65
+            $ref: '#/definitions/DefaultErrorBuilder'
66
+    post:
67
+      parameters:
68
+      - in: body
69
+        name: body
70
+        schema:
71
+          $ref: '#/definitions/HelloJsonSchema'
72
+      - in: path
73
+        name: name
74
+        required: true
75
+        type: string
76
+      responses:
77
+        '200':
78
+          description: '200'
79
+          schema:
80
+            $ref: '#/definitions/HelloResponseSchema'
81
+  /hello3/{name}:
82
+    get:
83
+      parameters:
84
+      - in: path
85
+        name: name
86
+        required: true
87
+        type: string
88
+      responses:
89
+        '200':
90
+          description: '200'
91
+          schema:
92
+            $ref: '#/definitions/HelloResponseSchema'
93
+swagger: '2.0'
94
+tags: []

File diff suppressed because it is too large
+ 87 - 0
hapic/static/swaggerui/swagger-ui-bundle.js


File diff suppressed because it is too large
+ 13 - 0
hapic/static/swaggerui/swagger-ui-standalone-preset.js


File diff suppressed because it is too large
+ 2 - 0
hapic/static/swaggerui/swagger-ui.css


File diff suppressed because it is too large
+ 8 - 0
hapic/static/swaggerui/swagger-ui.js


+ 2 - 1
tests/base.py View File

48
 
48
 
49
     def get_response(
49
     def get_response(
50
         self,
50
         self,
51
-        response: dict,
51
+        response: str,
52
         http_code: int,
52
         http_code: int,
53
+        mimetype: str='application/json',
53
     ) -> typing.Any:
54
     ) -> typing.Any:
54
         return {
55
         return {
55
             'original_response': response,
56
             'original_response': response,