processor.py 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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. self.path_parameters = path_parameters
  41. self.query_parameters = query_parameters
  42. self.body_parameters = body_parameters
  43. self.form_parameters = form_parameters
  44. self.header_parameters = header_parameters
  45. self.files_parameters = files_parameters
  46. class ProcessValidationError(object):
  47. def __init__(
  48. self,
  49. message: str,
  50. details: dict,
  51. ) -> None:
  52. self.message = message
  53. self.details = details
  54. class ProcessorInterface(object):
  55. def __init__(self):
  56. self._schema = None
  57. @property
  58. def schema(self):
  59. if not self._schema:
  60. raise ConfigurationException('Schema not set for processor {}'.format(str(self)))
  61. return self._schema
  62. @schema.setter
  63. def schema(self, schema):
  64. self._schema = schema
  65. def process(self, value):
  66. raise NotImplementedError
  67. def get_validation_error(
  68. self,
  69. request_context: RequestParameters,
  70. ) -> ProcessValidationError:
  71. raise NotImplementedError
  72. class Processor(ProcessorInterface):
  73. @classmethod
  74. def clean_data(cls, data: typing.Any) -> dict:
  75. # Fixes #22: Schemas make not validation if None is given
  76. if data is None:
  77. return {}
  78. return data
  79. class InputProcessor(Processor):
  80. pass
  81. class OutputProcessor(Processor):
  82. pass
  83. class MarshmallowOutputProcessor(OutputProcessor):
  84. def process(self, data: typing.Any):
  85. clean_data = self.clean_data(data)
  86. dump_data = self.schema.dump(clean_data).data
  87. self.validate(dump_data)
  88. return dump_data
  89. def validate(self, data: typing.Any) -> None:
  90. clean_data = self.clean_data(data)
  91. errors = self.schema.load(clean_data).errors
  92. if errors:
  93. raise OutputValidationException(
  94. 'Error when validate input: {}'.format(
  95. str(errors),
  96. )
  97. )
  98. def get_validation_error(self, data: dict) -> ProcessValidationError:
  99. clean_data = self.clean_data(data)
  100. dump_data = self.schema.dump(clean_data).data
  101. errors = self.schema.load(dump_data).errors
  102. return ProcessValidationError(
  103. message='Validation error of output data',
  104. details=errors,
  105. )
  106. class MarshmallowInputProcessor(InputProcessor):
  107. def process(self, data: dict):
  108. clean_data = self.clean_data(data)
  109. unmarshall = self.schema.load(clean_data)
  110. if unmarshall.errors:
  111. raise OutputValidationException(
  112. 'Error when validate ouput: {}'.format(
  113. str(unmarshall.errors),
  114. )
  115. )
  116. return unmarshall.data
  117. def get_validation_error(self, data: dict) -> ProcessValidationError:
  118. clean_data = self.clean_data(data)
  119. marshmallow_errors = self.schema.load(clean_data).errors
  120. return ProcessValidationError(
  121. message='Validation error of input data',
  122. details=marshmallow_errors,
  123. )
  124. class MarshmallowInputFilesProcessor(MarshmallowInputProcessor):
  125. def process(self, data: dict):
  126. clean_data = self.clean_data(data)
  127. unmarshall = self.schema.load(clean_data)
  128. additional_errors = self._get_files_errors(unmarshall.data)
  129. if unmarshall.errors:
  130. raise OutputValidationException(
  131. 'Error when validate ouput: {}'.format(
  132. str(unmarshall.errors),
  133. )
  134. )
  135. if additional_errors:
  136. raise OutputValidationException(
  137. 'Error when validate ouput: {}'.format(
  138. str(additional_errors),
  139. )
  140. )
  141. return unmarshall.data
  142. def get_validation_error(self, data: dict) -> ProcessValidationError:
  143. clean_data = self.clean_data(data)
  144. unmarshall = self.schema.load(clean_data)
  145. marshmallow_errors = unmarshall.errors
  146. additional_errors = self._get_files_errors(unmarshall.data)
  147. if marshmallow_errors:
  148. return ProcessValidationError(
  149. message='Validation error of input data',
  150. details=marshmallow_errors,
  151. )
  152. if additional_errors:
  153. return ProcessValidationError(
  154. message='Validation error of input data',
  155. details=additional_errors,
  156. )
  157. def _get_files_errors(self, validated_data: dict) -> typing.Dict[str, str]:
  158. """
  159. Additional check of data
  160. :param validated_data: previously validated data by marshmallow schema
  161. :return: list of error if any
  162. """
  163. errors = {}
  164. for field_name, field in self.schema.fields.items():
  165. # Actually just check if value not empty
  166. # TODO BS 20171102: Think about case where test file content is more complicated
  167. if field.required and (field_name not in validated_data or not validated_data[field_name]):
  168. errors.setdefault(field_name, []).append('Missing data for required field')
  169. return errors