resource.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. from flask import request
  4. from flask.views import MethodView
  5. from werkzeug import __version__ as werkzeug_version
  6. if werkzeug_version.split('.')[0] >= '2':
  7. from werkzeug.wrappers import Response as BaseResponse
  8. else:
  9. from werkzeug.wrappers import BaseResponse
  10. from .model import ModelBase
  11. from .utils import unpack
  12. class Resource(MethodView):
  13. """
  14. Represents an abstract RESTX resource.
  15. Concrete resources should extend from this class
  16. and expose methods for each supported HTTP method.
  17. If a resource is invoked with an unsupported HTTP method,
  18. the API will return a response with status 405 Method Not Allowed.
  19. Otherwise the appropriate method is called and passed all arguments
  20. from the url rule used when adding the resource to an Api instance.
  21. See :meth:`~flask_restx.Api.add_resource` for details.
  22. """
  23. representations = None
  24. method_decorators = []
  25. def __init__(self, api=None, *args, **kwargs):
  26. self.api = api
  27. def dispatch_request(self, *args, **kwargs):
  28. # Taken from flask
  29. meth = getattr(self, request.method.lower(), None)
  30. if meth is None and request.method == "HEAD":
  31. meth = getattr(self, "get", None)
  32. assert meth is not None, "Unimplemented method %r" % request.method
  33. for decorator in self.method_decorators:
  34. meth = decorator(meth)
  35. self.validate_payload(meth)
  36. resp = meth(*args, **kwargs)
  37. if isinstance(resp, BaseResponse):
  38. return resp
  39. representations = self.representations or {}
  40. mediatype = request.accept_mimetypes.best_match(representations, default=None)
  41. if mediatype in representations:
  42. data, code, headers = unpack(resp)
  43. resp = representations[mediatype](data, code, headers)
  44. resp.headers["Content-Type"] = mediatype
  45. return resp
  46. return resp
  47. def __validate_payload(self, expect, collection=False):
  48. """
  49. :param ModelBase expect: the expected model for the input payload
  50. :param bool collection: False if a single object of a resource is
  51. expected, True if a collection of objects of a resource is expected.
  52. """
  53. # TODO: proper content negotiation
  54. data = request.get_json()
  55. if collection:
  56. data = data if isinstance(data, list) else [data]
  57. for obj in data:
  58. expect.validate(obj, self.api.refresolver, self.api.format_checker)
  59. else:
  60. expect.validate(data, self.api.refresolver, self.api.format_checker)
  61. def validate_payload(self, func):
  62. """Perform a payload validation on expected model if necessary"""
  63. if getattr(func, "__apidoc__", False) is not False:
  64. doc = func.__apidoc__
  65. validate = doc.get("validate", None)
  66. validate = validate if validate is not None else self.api._validate
  67. if validate:
  68. for expect in doc.get("expect", []):
  69. # TODO: handle third party handlers
  70. if isinstance(expect, list) and len(expect) == 1:
  71. if isinstance(expect[0], ModelBase):
  72. self.__validate_payload(expect[0], collection=True)
  73. if isinstance(expect, ModelBase):
  74. self.__validate_payload(expect, collection=False)