processor.py 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- coding: utf-8 -*-
  2. import typing
  3. from multidict import MultiDict
  4. from hapic.exception import OutputValidationException
  5. from hapic.exception import ConfigurationException
  6. class RequestParameters(object):
  7. def __init__(
  8. self,
  9. path_parameters: dict,
  10. query_parameters: MultiDict,
  11. body_parameters: dict,
  12. form_parameters: MultiDict,
  13. header_parameters: dict,
  14. files_parameters: dict,
  15. ):
  16. """
  17. :param path_parameters: Parameters found in path, example:
  18. (for '/users/<user_id>') '/users/42' =>{'user_id': '42'}
  19. :param query_parameters: Parameters found in query, example:
  20. '/foo?group_id=1&group_id=2&deleted=false' => MultiDict(
  21. (
  22. ('group_id', '1'),
  23. ('group_id', '2'),
  24. ('deleted', 'false'),
  25. )
  26. )
  27. :param body_parameters: Body content in dict format, example:
  28. JSON body '{"user": {"name":"bob"}}' => {'user': {'name':'bob'}}
  29. :param form_parameters: Form parameters, example:
  30. <input type="text" name="name" value="bob"/> => {'name': 'bob'}
  31. :param header_parameters: headers in dict format, example:
  32. Connection: keep-alive
  33. Content-Type: text/plain => {
  34. 'Connection': 'keep-alive',
  35. 'Content-Type': 'text/plain',
  36. }
  37. :param files_parameters: TODO BS 20171113: Specify type of file
  38. storage ?
  39. """
  40. assert isinstance(query_parameters, MultiDict)
  41. assert isinstance(form_parameters, MultiDict)
  42. self.path_parameters = path_parameters
  43. self.query_parameters = query_parameters
  44. self.body_parameters = body_parameters
  45. self.form_parameters = form_parameters
  46. self.header_parameters = header_parameters
  47. self.files_parameters = files_parameters
  48. class ProcessValidationError(object):
  49. def __init__(
  50. self,
  51. message: str,
  52. details: dict,
  53. ) -> None:
  54. self.message = message
  55. self.details = details
  56. class ProcessorInterface(object):
  57. def __init__(self):
  58. self._schema = None
  59. @property
  60. def schema(self):
  61. if not self._schema:
  62. raise ConfigurationException('Schema not set for processor {}'.format(str(self)))
  63. return self._schema
  64. @schema.setter
  65. def schema(self, schema):
  66. self._schema = schema
  67. def process(self, value):
  68. raise NotImplementedError
  69. def get_validation_error(
  70. self,
  71. request_context: RequestParameters,
  72. ) -> ProcessValidationError:
  73. raise NotImplementedError
  74. class Processor(ProcessorInterface):
  75. @classmethod
  76. def clean_data(cls, data: typing.Any) -> dict:
  77. # Fixes #22: Schemas make not validation if None is given
  78. if data is None:
  79. return {}
  80. return data
  81. class InputProcessor(Processor):
  82. pass
  83. class OutputProcessor(Processor):
  84. pass
  85. class MarshmallowOutputProcessor(OutputProcessor):
  86. def process(self, data: typing.Any):
  87. clean_data = self.clean_data(data)
  88. dump_data = self.schema.dump(clean_data).data
  89. self.validate(dump_data)
  90. return dump_data
  91. def validate(self, data: typing.Any) -> None:
  92. clean_data = self.clean_data(data)
  93. errors = self.schema.load(clean_data).errors
  94. if errors:
  95. raise OutputValidationException(
  96. 'Error when validate input: {}'.format(
  97. str(errors),
  98. )
  99. )
  100. def get_validation_error(self, data: dict) -> ProcessValidationError:
  101. clean_data = self.clean_data(data)
  102. dump_data = self.schema.dump(clean_data).data
  103. errors = self.schema.load(dump_data).errors
  104. return ProcessValidationError(
  105. message='Validation error of output data',
  106. details=errors,
  107. )
  108. class MarshmallowInputProcessor(InputProcessor):
  109. def process(self, data: dict):
  110. clean_data = self.clean_data(data)
  111. unmarshall = self.schema.load(clean_data)
  112. if unmarshall.errors:
  113. raise OutputValidationException(
  114. 'Error when validate ouput: {}'.format(
  115. str(unmarshall.errors),
  116. )
  117. )
  118. return unmarshall.data
  119. def get_validation_error(self, data: dict) -> ProcessValidationError:
  120. clean_data = self.clean_data(data)
  121. marshmallow_errors = self.schema.load(clean_data).errors
  122. return ProcessValidationError(
  123. message='Validation error of input data',
  124. details=marshmallow_errors,
  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. if unmarshall.errors:
  132. raise OutputValidationException(
  133. 'Error when validate ouput: {}'.format(
  134. str(unmarshall.errors),
  135. )
  136. )
  137. if additional_errors:
  138. raise OutputValidationException(
  139. 'Error when validate ouput: {}'.format(
  140. str(additional_errors),
  141. )
  142. )
  143. return unmarshall.data
  144. def get_validation_error(self, data: dict) -> ProcessValidationError:
  145. clean_data = self.clean_data(data)
  146. unmarshall = self.schema.load(clean_data)
  147. marshmallow_errors = unmarshall.errors
  148. additional_errors = self._get_files_errors(unmarshall.data)
  149. if marshmallow_errors:
  150. return ProcessValidationError(
  151. message='Validation error of input data',
  152. details=marshmallow_errors,
  153. )
  154. if additional_errors:
  155. return ProcessValidationError(
  156. message='Validation error of input data',
  157. details=additional_errors,
  158. )
  159. def _get_files_errors(self, validated_data: dict) -> typing.Dict[str, str]:
  160. """
  161. Additional check of data
  162. :param validated_data: previously validated data by marshmallow schema
  163. :return: list of error if any
  164. """
  165. errors = {}
  166. for field_name, field in self.schema.fields.items():
  167. # Actually just check if value not empty
  168. # TODO BS 20171102: Think about case where test file content is more complicated
  169. if field.required and (field_name not in validated_data or not validated_data[field_name]):
  170. errors.setdefault(field_name, []).append('Missing data for required field')
  171. return errors