processor.py 5.8KB

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