Browse Source

Add input_files and output_file

Bastien Sevajol 6 years ago
parent
commit
704925590e

+ 22 - 0
hapic/buffer.py View File

7
 from hapic.description import InputBodyDescription
7
 from hapic.description import InputBodyDescription
8
 from hapic.description import InputHeadersDescription
8
 from hapic.description import InputHeadersDescription
9
 from hapic.description import InputFormsDescription
9
 from hapic.description import InputFormsDescription
10
+from hapic.description import InputFilesDescription
10
 from hapic.description import OutputBodyDescription
11
 from hapic.description import OutputBodyDescription
12
+from hapic.description import OutputFileDescription
11
 from hapic.description import OutputHeadersDescription
13
 from hapic.description import OutputHeadersDescription
12
 from hapic.description import ErrorDescription
14
 from hapic.description import ErrorDescription
13
 from hapic.exception import AlreadyDecoratedException
15
 from hapic.exception import AlreadyDecoratedException
74
         self._description.input_forms = description
76
         self._description.input_forms = description
75
 
77
 
76
     @property
78
     @property
79
+    def input_files(self) -> InputFilesDescription:
80
+        return self._description.input_files
81
+
82
+    @input_files.setter
83
+    def input_files(self, description: InputFilesDescription) -> None:
84
+        if self._description.input_files is not None:
85
+            raise AlreadyDecoratedException()
86
+        self._description.input_files = description
87
+
88
+    @property
77
     def output_body(self) -> OutputBodyDescription:
89
     def output_body(self) -> OutputBodyDescription:
78
         return self._description.output_body
90
         return self._description.output_body
79
 
91
 
84
         self._description.output_body = description
96
         self._description.output_body = description
85
 
97
 
86
     @property
98
     @property
99
+    def output_file(self) -> OutputFileDescription:
100
+        return self._description.output_file
101
+
102
+    @output_file.setter
103
+    def output_file(self, description: OutputFileDescription) -> None:
104
+        if self._description.output_file is not None:
105
+            raise AlreadyDecoratedException()
106
+        self._description.output_file = description
107
+
108
+    @property
87
     def output_headers(self) -> OutputHeadersDescription:
109
     def output_headers(self) -> OutputHeadersDescription:
88
         return self._description.output_headers
110
         return self._description.output_headers
89
 
111
 

+ 1 - 0
hapic/data.py View File

8
         self.query = {}
8
         self.query = {}
9
         self.headers = {}
9
         self.headers = {}
10
         self.forms = {}
10
         self.forms = {}
11
+        self.files = {}

+ 38 - 43
hapic/decorator.py View File

150
         self,
150
         self,
151
         request_parameters: RequestParameters,
151
         request_parameters: RequestParameters,
152
     ) -> typing.Any:
152
     ) -> typing.Any:
153
+        parameters_data = self.get_parameters_data(request_parameters)
154
+        processed_data = self.processor.process(parameters_data)
155
+        return processed_data
156
+
157
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
153
         raise NotImplementedError()
158
         raise NotImplementedError()
154
 
159
 
155
     def update_hapic_data(
160
     def update_hapic_data(
163
         self,
168
         self,
164
         request_parameters: RequestParameters,
169
         request_parameters: RequestParameters,
165
     ) -> typing.Any:
170
     ) -> typing.Any:
166
-        error = self.processor.get_validation_error(
167
-            request_parameters.body_parameters,
168
-        )
171
+        parameters_data = self.get_parameters_data(request_parameters)
172
+        error = self.processor.get_validation_error(parameters_data)
169
         error_response = self.context.get_validation_error_response(
173
         error_response = self.context.get_validation_error_response(
170
             error,
174
             error,
171
             http_code=self.error_http_code,
175
             http_code=self.error_http_code,
249
     pass
253
     pass
250
 
254
 
251
 
255
 
256
+class OutputFileControllerWrapper(ControllerWrapper):
257
+    def __init__(
258
+        self,
259
+        output_type: str,
260
+        default_http_code: HTTPStatus=HTTPStatus.OK,
261
+    ) -> None:
262
+        self.output_type = output_type
263
+        self.default_http_code = default_http_code
264
+
265
+
252
 class InputPathControllerWrapper(InputControllerWrapper):
266
 class InputPathControllerWrapper(InputControllerWrapper):
253
     def update_hapic_data(
267
     def update_hapic_data(
254
         self, hapic_data: HapicData,
268
         self, hapic_data: HapicData,
256
     ) -> None:
270
     ) -> None:
257
         hapic_data.path = processed_data
271
         hapic_data.path = processed_data
258
 
272
 
259
-    def get_processed_data(
260
-        self,
261
-        request_parameters: RequestParameters,
262
-    ) -> typing.Any:
263
-        processed_data = self.processor.process(
264
-            request_parameters.path_parameters,
265
-        )
266
-        return processed_data
273
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
274
+        return request_parameters.path_parameters
267
 
275
 
268
 
276
 
269
 class InputQueryControllerWrapper(InputControllerWrapper):
277
 class InputQueryControllerWrapper(InputControllerWrapper):
273
     ) -> None:
281
     ) -> None:
274
         hapic_data.query = processed_data
282
         hapic_data.query = processed_data
275
 
283
 
276
-    def get_processed_data(
277
-        self,
278
-        request_parameters: RequestParameters,
279
-    ) -> typing.Any:
280
-        processed_data = self.processor.process(
281
-            request_parameters.query_parameters,
282
-        )
283
-        return processed_data
284
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
285
+        return request_parameters.query_parameters
284
 
286
 
285
 
287
 
286
 class InputBodyControllerWrapper(InputControllerWrapper):
288
 class InputBodyControllerWrapper(InputControllerWrapper):
290
     ) -> None:
292
     ) -> None:
291
         hapic_data.body = processed_data
293
         hapic_data.body = processed_data
292
 
294
 
293
-    def get_processed_data(
294
-        self,
295
-        request_parameters: RequestParameters,
296
-    ) -> typing.Any:
297
-        processed_data = self.processor.process(
298
-            request_parameters.body_parameters,
299
-        )
300
-        return processed_data
295
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
296
+        return request_parameters.body_parameters
301
 
297
 
302
 
298
 
303
 class InputHeadersControllerWrapper(InputControllerWrapper):
299
 class InputHeadersControllerWrapper(InputControllerWrapper):
307
     ) -> None:
303
     ) -> None:
308
         hapic_data.headers = processed_data
304
         hapic_data.headers = processed_data
309
 
305
 
310
-    def get_processed_data(
311
-        self,
312
-        request_parameters: RequestParameters,
313
-    ) -> typing.Any:
314
-        processed_data = self.processor.process(
315
-            request_parameters.header_parameters,
316
-        )
317
-        return processed_data
306
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
307
+        return request_parameters.header_parameters
318
 
308
 
319
 
309
 
320
 class InputFormsControllerWrapper(InputControllerWrapper):
310
 class InputFormsControllerWrapper(InputControllerWrapper):
324
     ) -> None:
314
     ) -> None:
325
         hapic_data.forms = processed_data
315
         hapic_data.forms = processed_data
326
 
316
 
327
-    def get_processed_data(
328
-        self,
329
-        request_parameters: RequestParameters,
330
-    ) -> typing.Any:
331
-        processed_data = self.processor.process(
332
-            request_parameters.form_parameters,
333
-        )
334
-        return processed_data
317
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
318
+        return request_parameters.form_parameters
319
+
320
+
321
+class InputFilesControllerWrapper(InputControllerWrapper):
322
+    def update_hapic_data(
323
+        self, hapic_data: HapicData,
324
+        processed_data: typing.Any,
325
+    ) -> None:
326
+        hapic_data.files = processed_data
327
+
328
+    def get_parameters_data(self, request_parameters: RequestParameters) -> dict:
329
+        return request_parameters.files_parameters
335
 
330
 
336
 
331
 
337
 class ExceptionHandlerControllerWrapper(ControllerWrapper):
332
 class ExceptionHandlerControllerWrapper(ControllerWrapper):

+ 13 - 2
hapic/description.py View File

27
 
27
 
28
 
28
 
29
 class InputFormsDescription(Description):
29
 class InputFormsDescription(Description):
30
-    def __init__(self, wrapper: 'ControllerWrapper') -> None:
31
-        self.wrapper = wrapper
30
+    pass
31
+
32
+
33
+class InputFilesDescription(Description):
34
+    pass
32
 
35
 
33
 
36
 
34
 class OutputBodyDescription(Description):
37
 class OutputBodyDescription(Description):
35
     pass
38
     pass
36
 
39
 
37
 
40
 
41
+class OutputFileDescription(Description):
42
+    pass
43
+
44
+
38
 class OutputHeadersDescription(Description):
45
 class OutputHeadersDescription(Description):
39
     pass
46
     pass
40
 
47
 
51
         input_body: InputBodyDescription=None,
58
         input_body: InputBodyDescription=None,
52
         input_headers: InputHeadersDescription=None,
59
         input_headers: InputHeadersDescription=None,
53
         input_forms: InputFormsDescription=None,
60
         input_forms: InputFormsDescription=None,
61
+        input_files: InputFilesDescription=None,
54
         output_body: OutputBodyDescription=None,
62
         output_body: OutputBodyDescription=None,
63
+        output_file: OutputFileDescription=None,
55
         output_headers: OutputHeadersDescription=None,
64
         output_headers: OutputHeadersDescription=None,
56
         errors: typing.List[ErrorDescription]=None,
65
         errors: typing.List[ErrorDescription]=None,
57
     ):
66
     ):
60
         self.input_body = input_body
69
         self.input_body = input_body
61
         self.input_headers = input_headers
70
         self.input_headers = input_headers
62
         self.input_forms = input_forms
71
         self.input_forms = input_forms
72
+        self.input_files = input_files
63
         self.output_body = output_body
73
         self.output_body = output_body
74
+        self.output_file = output_file
64
         self.output_headers = output_headers
75
         self.output_headers = output_headers
65
         self.errors = errors or []
76
         self.errors = errors or []

+ 20 - 1
hapic/doc.py View File

22
     app: bottle.Bottle,
22
     app: bottle.Bottle,
23
 ):
23
 ):
24
     if not app.routes:
24
     if not app.routes:
25
-        raise NoRoutesException('There is no routes in yout bottle app')
25
+        raise NoRoutesException('There is no routes in your bottle app')
26
 
26
 
27
     reference = decorated_controller.reference
27
     reference = decorated_controller.reference
28
     for route in app.routes:
28
     for route in app.routes:
74
                 }
74
                 }
75
             }
75
             }
76
 
76
 
77
+    if description.output_file:
78
+        method_operations.setdefault('produce', []).append(
79
+            description.output_file.wrapper.output_type
80
+        )
81
+        method_operations.setdefault('responses', {})\
82
+            [int(description.output_file.wrapper.default_http_code)] = {
83
+            'description': str(description.output_file.wrapper.default_http_code),  # nopep8
84
+        }
85
+
77
     if description.errors:
86
     if description.errors:
78
         for error in description.errors:
87
         for error in description.errors:
79
             schema_class = type(error.wrapper.schema)
88
             schema_class = type(error.wrapper.schema)
109
                 'type': schema['type']
118
                 'type': schema['type']
110
             })
119
             })
111
 
120
 
121
+    if description.input_files:
122
+        method_operations.setdefault('consume', []).append('multipart/form-data')
123
+        for field_name, field in description.input_files.wrapper.processor.schema.fields.items():
124
+            method_operations.setdefault('parameters', []).append({
125
+                'in': 'formData',
126
+                'name': field_name,
127
+                'required': field.required,
128
+                'type': 'file',
129
+            })
130
+
112
     operations = {
131
     operations = {
113
         bottle_route.method.lower(): method_operations,
132
         bottle_route.method.lower(): method_operations,
114
     }
133
     }

+ 4 - 0
hapic/exception.py View File

5
     pass
5
     pass
6
 
6
 
7
 
7
 
8
+class ConfigurationException(HapicException):
9
+    pass
10
+
11
+
8
 class WorkflowException(HapicException):
12
 class WorkflowException(HapicException):
9
     pass
13
     pass
10
 
14
 

+ 45 - 0
hapic/hapic.py View File

16
 from hapic.decorator import InputHeadersControllerWrapper
16
 from hapic.decorator import InputHeadersControllerWrapper
17
 from hapic.decorator import InputPathControllerWrapper
17
 from hapic.decorator import InputPathControllerWrapper
18
 from hapic.decorator import InputQueryControllerWrapper
18
 from hapic.decorator import InputQueryControllerWrapper
19
+from hapic.decorator import InputFilesControllerWrapper
19
 from hapic.decorator import OutputBodyControllerWrapper
20
 from hapic.decorator import OutputBodyControllerWrapper
20
 from hapic.decorator import OutputHeadersControllerWrapper
21
 from hapic.decorator import OutputHeadersControllerWrapper
22
+from hapic.decorator import OutputFileControllerWrapper
21
 from hapic.description import InputBodyDescription
23
 from hapic.description import InputBodyDescription
22
 from hapic.description import ErrorDescription
24
 from hapic.description import ErrorDescription
23
 from hapic.description import InputFormsDescription
25
 from hapic.description import InputFormsDescription
24
 from hapic.description import InputHeadersDescription
26
 from hapic.description import InputHeadersDescription
25
 from hapic.description import InputPathDescription
27
 from hapic.description import InputPathDescription
26
 from hapic.description import InputQueryDescription
28
 from hapic.description import InputQueryDescription
29
+from hapic.description import InputFilesDescription
27
 from hapic.description import OutputBodyDescription
30
 from hapic.description import OutputBodyDescription
28
 from hapic.description import OutputHeadersDescription
31
 from hapic.description import OutputHeadersDescription
32
+from hapic.description import OutputFileDescription
29
 from hapic.doc import DocGenerator
33
 from hapic.doc import DocGenerator
30
 from hapic.processor import ProcessorInterface
34
 from hapic.processor import ProcessorInterface
31
 from hapic.processor import MarshmallowInputProcessor
35
 from hapic.processor import MarshmallowInputProcessor
36
+from hapic.processor import MarshmallowInputFilesProcessor
32
 from hapic.processor import MarshmallowOutputProcessor
37
 from hapic.processor import MarshmallowOutputProcessor
33
 
38
 
34
 
39
 
148
             return decoration.get_wrapper(func)
153
             return decoration.get_wrapper(func)
149
         return decorator
154
         return decorator
150
 
155
 
156
+    # TODO BS 20171102: Think about possibilities to validate output ? (with mime type, or validator)
157
+    def output_file(
158
+        self,
159
+        output_type: str,
160
+        default_http_code: HTTPStatus = HTTPStatus.OK,
161
+    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
162
+        decoration = OutputFileControllerWrapper(
163
+            output_type=output_type,
164
+            default_http_code=default_http_code,
165
+        )
166
+
167
+        def decorator(func):
168
+            self._buffer.output_file = OutputFileDescription(decoration)
169
+            return decoration.get_wrapper(func)
170
+        return decorator
171
+
151
     def input_headers(
172
     def input_headers(
152
         self,
173
         self,
153
         schema: typing.Any,
174
         schema: typing.Any,
268
             return decoration.get_wrapper(func)
289
             return decoration.get_wrapper(func)
269
         return decorator
290
         return decorator
270
 
291
 
292
+    def input_files(
293
+        self,
294
+        schema: typing.Any,
295
+        processor: ProcessorInterface=None,
296
+        context: ContextInterface=None,
297
+        error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
298
+        default_http_code: HTTPStatus = HTTPStatus.OK,
299
+    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
300
+        processor = processor or MarshmallowInputFilesProcessor()
301
+        processor.schema = schema
302
+        context = context or self._context_getter
303
+
304
+        decoration = InputFilesControllerWrapper(
305
+            context=context,
306
+            processor=processor,
307
+            error_http_code=error_http_code,
308
+            default_http_code=default_http_code,
309
+        )
310
+
311
+        def decorator(func):
312
+            self._buffer.input_files = InputFilesDescription(decoration)
313
+            return decoration.get_wrapper(func)
314
+        return decorator
315
+
271
     def handle_exception(
316
     def handle_exception(
272
         self,
317
         self,
273
         handled_exception_class: typing.Type[Exception],
318
         handled_exception_class: typing.Type[Exception],

+ 72 - 2
hapic/processor.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import typing
2
 import typing
3
 
3
 
4
-from hapic.exception import InputValidationException, OutputValidationException
4
+from hapic.exception import OutputValidationException
5
+from hapic.exception import ConfigurationException
5
 
6
 
6
 
7
 
7
 class RequestParameters(object):
8
 class RequestParameters(object):
12
         body_parameters,
13
         body_parameters,
13
         form_parameters,
14
         form_parameters,
14
         header_parameters,
15
         header_parameters,
16
+        files_parameters,
15
     ):
17
     ):
16
         self.path_parameters = path_parameters
18
         self.path_parameters = path_parameters
17
         self.query_parameters = query_parameters
19
         self.query_parameters = query_parameters
18
         self.body_parameters = body_parameters
20
         self.body_parameters = body_parameters
19
         self.form_parameters = form_parameters
21
         self.form_parameters = form_parameters
20
         self.header_parameters = header_parameters
22
         self.header_parameters = header_parameters
23
+        self.files_parameters = files_parameters
21
 
24
 
22
 
25
 
23
 class ProcessValidationError(object):
26
 class ProcessValidationError(object):
32
 
35
 
33
 class ProcessorInterface(object):
36
 class ProcessorInterface(object):
34
     def __init__(self):
37
     def __init__(self):
35
-        self.schema = None
38
+        self._schema = None
39
+
40
+    @property
41
+    def schema(self):
42
+        if not self._schema:
43
+            raise ConfigurationException('Schema not set for processor {}'.format(str(self)))
44
+        return self._schema
45
+
46
+    @schema.setter
47
+    def schema(self, schema):
48
+        self._schema = schema
36
 
49
 
37
     def process(self, value):
50
     def process(self, value):
38
         raise NotImplementedError
51
         raise NotImplementedError
108
             message='Validation error of input data',
121
             message='Validation error of input data',
109
             details=marshmallow_errors,
122
             details=marshmallow_errors,
110
         )
123
         )
124
+
125
+
126
+class MarshmallowInputFilesProcessor(MarshmallowInputProcessor):
127
+    def process(self, data: dict):
128
+        clean_data = self.clean_data(data)
129
+        unmarshall = self.schema.load(clean_data)
130
+        additional_errors = self._get_files_errors(unmarshall.data)
131
+
132
+        if unmarshall.errors:
133
+            raise OutputValidationException(
134
+                'Error when validate ouput: {}'.format(
135
+                    str(unmarshall.errors),
136
+                )
137
+            )
138
+
139
+        if additional_errors:
140
+            raise OutputValidationException(
141
+                'Error when validate ouput: {}'.format(
142
+                    str(additional_errors),
143
+                )
144
+            )
145
+
146
+        return unmarshall.data
147
+
148
+    def get_validation_error(self, data: dict) -> ProcessValidationError:
149
+        clean_data = self.clean_data(data)
150
+        unmarshall = self.schema.load(clean_data)
151
+        marshmallow_errors = unmarshall.errors
152
+        additional_errors = self._get_files_errors(unmarshall.data)
153
+
154
+        if marshmallow_errors:
155
+            return ProcessValidationError(
156
+                message='Validation error of input data',
157
+                details=marshmallow_errors,
158
+            )
159
+
160
+        if additional_errors:
161
+            return ProcessValidationError(
162
+                message='Validation error of input data',
163
+                details=additional_errors,
164
+            )
165
+
166
+    def _get_files_errors(self, validated_data: dict) -> typing.Dict[str, str]:
167
+        """
168
+        Additional check of data
169
+        :param validated_data: previously validated data by marshmallow schema
170
+        :return: list of error if any
171
+        """
172
+        errors = {}
173
+
174
+        for field_name, field in self.schema.fields.items():
175
+            # Actually just check if value not empty
176
+            # TODO BS 20171102: Think about case where test file content is more complicated
177
+            if field.required and (field_name not in validated_data or not validated_data[field_name]):
178
+                errors.setdefault(field_name, []).append('Missing data for required field')
179
+
180
+        return errors

+ 55 - 0
tests/base.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import typing
3
+from http import HTTPStatus
4
+
5
+from hapic.context import ContextInterface
6
+from hapic.processor import RequestParameters
7
+from hapic.processor import ProcessValidationError
8
+
2
 
9
 
3
 class Base(object):
10
 class Base(object):
4
     pass
11
     pass
12
+
13
+
14
+class MyContext(ContextInterface):
15
+    def __init__(
16
+        self,
17
+        fake_path_parameters=None,
18
+        fake_query_parameters=None,
19
+        fake_body_parameters=None,
20
+        fake_form_parameters=None,
21
+        fake_header_parameters=None,
22
+        fake_files_parameters=None,
23
+    ) -> None:
24
+        self.fake_path_parameters = fake_path_parameters or {}
25
+        self.fake_query_parameters = fake_query_parameters or {}
26
+        self.fake_body_parameters = fake_body_parameters or {}
27
+        self.fake_form_parameters = fake_form_parameters or {}
28
+        self.fake_header_parameters = fake_header_parameters or {}
29
+        self.fake_files_parameters = fake_files_parameters or {}
30
+
31
+    def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
32
+        return RequestParameters(
33
+            path_parameters=self.fake_path_parameters,
34
+            query_parameters=self.fake_query_parameters,
35
+            body_parameters=self.fake_body_parameters,
36
+            form_parameters=self.fake_form_parameters,
37
+            header_parameters=self.fake_header_parameters,
38
+            files_parameters=self.fake_files_parameters,
39
+        )
40
+
41
+    def get_response(
42
+        self,
43
+        response: dict,
44
+        http_code: int,
45
+    ) -> typing.Any:
46
+        return {
47
+            'original_response': response,
48
+            'http_code': http_code,
49
+        }
50
+
51
+    def get_validation_error_response(
52
+        self,
53
+        error: ProcessValidationError,
54
+        http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
55
+    ) -> typing.Any:
56
+        return {
57
+            'original_error': error,
58
+            'http_code': http_code,
59
+        }

+ 1 - 0
tests/func/__init__.py View File

1
+# coding: utf-8

+ 93 - 0
tests/func/test_doc.py View File

1
+# coding: utf-8
2
+import marshmallow
3
+import bottle
4
+
5
+from hapic import Hapic
6
+from tests.base import Base
7
+from tests.base import MyContext
8
+
9
+
10
+class TestDocGeneration(Base):
11
+    def test_func__input_files_doc__ok__one_file(self):
12
+        hapic = Hapic()
13
+        hapic.set_context(MyContext())
14
+        app = bottle.Bottle()
15
+
16
+        class MySchema(marshmallow.Schema):
17
+            file_abc = marshmallow.fields.Raw(required=True)
18
+
19
+        @hapic.with_api_doc()
20
+        @hapic.input_files(MySchema())
21
+        def my_controller(hapic_data=None):
22
+            assert hapic_data
23
+            assert hapic_data.files
24
+
25
+        app.route('/upload', method='POST', callback=my_controller)
26
+        doc = hapic.generate_doc(app)
27
+
28
+        assert doc
29
+        assert '/upload' in doc['paths']
30
+        assert 'consume' in doc['paths']['/upload']['post']
31
+        assert 'multipart/form-data' in doc['paths']['/upload']['post']['consume']
32
+        assert 'parameters' in doc['paths']['/upload']['post']
33
+        assert {
34
+                   'name': 'file_abc',
35
+                   'required': True,
36
+                   'in': 'formData',
37
+                   'type': 'file',
38
+               } in doc['paths']['/upload']['post']['parameters']
39
+
40
+    def test_func__input_files_doc__ok__two_file(self):
41
+        hapic = Hapic()
42
+        hapic.set_context(MyContext())
43
+        app = bottle.Bottle()
44
+
45
+        class MySchema(marshmallow.Schema):
46
+            file_abc = marshmallow.fields.Raw(required=True)
47
+            file_def = marshmallow.fields.Raw(required=False)
48
+
49
+        @hapic.with_api_doc()
50
+        @hapic.input_files(MySchema())
51
+        def my_controller(hapic_data=None):
52
+            assert hapic_data
53
+            assert hapic_data.files
54
+
55
+        app.route('/upload', method='POST', callback=my_controller)
56
+        doc = hapic.generate_doc(app)
57
+
58
+        assert doc
59
+        assert '/upload' in doc['paths']
60
+        assert 'consume' in doc['paths']['/upload']['post']
61
+        assert 'multipart/form-data' in doc['paths']['/upload']['post']['consume']
62
+        assert 'parameters' in doc['paths']['/upload']['post']
63
+        assert {
64
+                   'name': 'file_abc',
65
+                   'required': True,
66
+                   'in': 'formData',
67
+                   'type': 'file',
68
+               } in doc['paths']['/upload']['post']['parameters']
69
+        assert {
70
+                   'name': 'file_def',
71
+                   'required': False,
72
+                   'in': 'formData',
73
+                   'type': 'file',
74
+               } in doc['paths']['/upload']['post']['parameters']
75
+
76
+    def test_func__output_file_doc__ok__nominal_case(self):
77
+        hapic = Hapic()
78
+        hapic.set_context(MyContext())
79
+        app = bottle.Bottle()
80
+
81
+        @hapic.with_api_doc()
82
+        @hapic.output_file('image/jpeg')
83
+        def my_controller():
84
+            return b'101010100101'
85
+
86
+        app.route('/avatar', method='GET', callback=my_controller)
87
+        doc = hapic.generate_doc(app)
88
+
89
+        assert doc
90
+        assert '/avatar' in doc['paths']
91
+        assert 'produce' in doc['paths']['/avatar']['get']
92
+        assert 'image/jpeg' in doc['paths']['/avatar']['get']['produce']
93
+        assert 200 in doc['paths']['/avatar']['get']['responses']

+ 73 - 0
tests/func/test_marshmallow_decoration.py View File

1
+# coding: utf-8
2
+from http import HTTPStatus
3
+
4
+import marshmallow
5
+
6
+from hapic import Hapic
7
+from tests.base import Base
8
+from tests.base import MyContext
9
+
10
+
11
+class TestMarshmallowDecoration(Base):
12
+    def test_unit__input_files__ok__file_is_present(self):
13
+        hapic = Hapic()
14
+        hapic.set_context(MyContext(fake_files_parameters={
15
+            'file_abc': '10101010101',
16
+        }))
17
+
18
+        class MySchema(marshmallow.Schema):
19
+            file_abc = marshmallow.fields.Raw(required=True)
20
+
21
+        @hapic.with_api_doc()
22
+        @hapic.input_files(MySchema())
23
+        def my_controller(hapic_data=None):
24
+            assert hapic_data
25
+            assert hapic_data.files
26
+            return 'OK'
27
+
28
+        result = my_controller()
29
+        assert 'OK' == result
30
+
31
+    def test_unit__input_files__ok__file_is_not_present(self):
32
+        hapic = Hapic()
33
+        hapic.set_context(MyContext(fake_files_parameters={
34
+            # No file here
35
+        }))
36
+
37
+        class MySchema(marshmallow.Schema):
38
+            file_abc = marshmallow.fields.Raw(required=True)
39
+
40
+        @hapic.with_api_doc()
41
+        @hapic.input_files(MySchema())
42
+        def my_controller(hapic_data=None):
43
+            assert hapic_data
44
+            assert hapic_data.files
45
+            return 'OK'
46
+
47
+        result = my_controller()
48
+        assert 'http_code' in result
49
+        assert HTTPStatus.BAD_REQUEST == result['http_code']
50
+        assert {
51
+                   'file_abc': ['Missing data for required field.']
52
+               } == result['original_error'].details
53
+
54
+    def test_unit__input_files__ok__file_is_empty_string(self):
55
+        hapic = Hapic()
56
+        hapic.set_context(MyContext(fake_files_parameters={
57
+            'file_abc': '',
58
+        }))
59
+
60
+        class MySchema(marshmallow.Schema):
61
+            file_abc = marshmallow.fields.Raw(required=True)
62
+
63
+        @hapic.with_api_doc()
64
+        @hapic.input_files(MySchema())
65
+        def my_controller(hapic_data=None):
66
+            assert hapic_data
67
+            assert hapic_data.files
68
+            return 'OK'
69
+
70
+        result = my_controller()
71
+        assert 'http_code' in result
72
+        assert HTTPStatus.BAD_REQUEST == result['http_code']
73
+        assert {'file_abc': ['Missing data for required field']} == result['original_error'].details

+ 10 - 39
tests/unit/test_decorator.py View File

4
 
4
 
5
 import marshmallow
5
 import marshmallow
6
 
6
 
7
-from hapic.context import ContextInterface
8
 from hapic.data import HapicData
7
 from hapic.data import HapicData
9
-from hapic.decorator import InputOutputControllerWrapper
10
 from hapic.decorator import ExceptionHandlerControllerWrapper
8
 from hapic.decorator import ExceptionHandlerControllerWrapper
11
 from hapic.decorator import InputControllerWrapper
9
 from hapic.decorator import InputControllerWrapper
10
+from hapic.decorator import InputOutputControllerWrapper
12
 from hapic.decorator import OutputControllerWrapper
11
 from hapic.decorator import OutputControllerWrapper
13
 from hapic.hapic import ErrorResponseSchema
12
 from hapic.hapic import ErrorResponseSchema
14
-from hapic.processor import RequestParameters
15
 from hapic.processor import MarshmallowOutputProcessor
13
 from hapic.processor import MarshmallowOutputProcessor
16
 from hapic.processor import ProcessValidationError
14
 from hapic.processor import ProcessValidationError
17
 from hapic.processor import ProcessorInterface
15
 from hapic.processor import ProcessorInterface
16
+from hapic.processor import RequestParameters
18
 from tests.base import Base
17
 from tests.base import Base
19
-
20
-
21
-class MyContext(ContextInterface):
22
-    def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
23
-        return RequestParameters(
24
-            path_parameters={'fake': args},
25
-            query_parameters={},
26
-            body_parameters={},
27
-            form_parameters={},
28
-            header_parameters={},
29
-        )
30
-
31
-    def get_response(
32
-        self,
33
-        response: dict,
34
-        http_code: int,
35
-    ) -> typing.Any:
36
-        return {
37
-            'original_response': response,
38
-            'http_code': http_code,
39
-        }
40
-
41
-    def get_validation_error_response(
42
-        self,
43
-        error: ProcessValidationError,
44
-        http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
45
-    ) -> typing.Any:
46
-        return {
47
-            'original_error': error,
48
-            'http_code': http_code,
49
-        }
18
+from tests.base import MyContext
50
 
19
 
51
 
20
 
52
 class MyProcessor(ProcessorInterface):
21
 class MyProcessor(ProcessorInterface):
82
         return response * 2
51
         return response * 2
83
 
52
 
84
 
53
 
85
-class MyInputControllerWrapper(InputControllerWrapper):
54
+class MyInputQueryControllerWrapper(InputControllerWrapper):
86
     def get_processed_data(
55
     def get_processed_data(
87
         self,
56
         self,
88
         request_parameters: RequestParameters,
57
         request_parameters: RequestParameters,
89
     ) -> typing.Any:
58
     ) -> typing.Any:
90
-        return {'we_are_testing': request_parameters.path_parameters}
59
+        return request_parameters.query_parameters
91
 
60
 
92
     def update_hapic_data(
61
     def update_hapic_data(
93
         self,
62
         self,
146
 
115
 
147
 class TestInputControllerWrapper(Base):
116
 class TestInputControllerWrapper(Base):
148
     def test_unit__input_data_wrapping__ok__nominal_case(self):
117
     def test_unit__input_data_wrapping__ok__nominal_case(self):
149
-        context = MyContext()
118
+        context = MyContext(fake_query_parameters={
119
+            'foo': 'bar',
120
+        })
150
         processor = MyProcessor()
121
         processor = MyProcessor()
151
-        wrapper = MyInputControllerWrapper(context, processor)
122
+        wrapper = MyInputQueryControllerWrapper(context, processor)
152
 
123
 
153
         @wrapper.get_wrapper
124
         @wrapper.get_wrapper
154
         def func(foo, hapic_data=None):
125
         def func(foo, hapic_data=None):
155
             assert hapic_data
126
             assert hapic_data
156
             assert isinstance(hapic_data, HapicData)
127
             assert isinstance(hapic_data, HapicData)
157
             # see MyControllerWrapper#before_wrapped_func
128
             # see MyControllerWrapper#before_wrapped_func
158
-            assert hapic_data.query == {'we_are_testing': {'fake': (42,)}}
129
+            assert hapic_data.query == {'foo': 'bar'}
159
             return foo
130
             return foo
160
 
131
 
161
         result = func(42)
132
         result = func(42)