Skip to content

Commit

Permalink
feat(decorator): align response fmt for dj and drf
Browse files Browse the repository at this point in the history
  • Loading branch information
CJHwong committed Jun 7, 2024
1 parent 9d2d1f1 commit 50a2ac1
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 11 deletions.
16 changes: 10 additions & 6 deletions data_spec_validator/decorator/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
try:
from django.core.handlers.asgi import ASGIRequest
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseBadRequest, HttpResponseForbidden, QueryDict
from django.http import HttpResponseBadRequest, HttpResponseForbidden, JsonResponse, QueryDict
from django.views.generic.base import View
except ModuleNotFoundError as e:
print(f'[DSV][WARNING] decorator: "dsv" cannot be used, {e}')
Expand Down Expand Up @@ -150,11 +150,11 @@ def _do_validate(data, spec, multirow):
is_multirow = _eval_is_multirow(multirow, data)
validate_data_spec(data, spec, multirow=is_multirow)
except ValueError as value_err:
error = ValidationError(str(value_err.args))
error = ValidationError(value_err.args)
except PermissionError as perm_err:
error = PermissionDenied(str(perm_err.args))
error = PermissionDenied(perm_err.args)
except (LookupError, TypeError, RuntimeError, DSVError) as parse_err:
error = ParseError(str(parse_err.args))
error = ParseError(parse_err.args)

if error:
raise error
Expand All @@ -165,20 +165,24 @@ def _get_error_response(error, use_drf):
Return the error response based on the error type.
If the attribute use_drf is True, Raise DRF's exception to let DRF's exception handler do something about it.
"""
error_msg = {'messages': error.message}

if use_drf:
err_map = {
ValidationError: drf_exceptions.ValidationError,
PermissionDenied: drf_exceptions.PermissionDenied,
ParseError: drf_exceptions.ParseError,
}
raise err_map[error.__class__](error.message)
raise err_map[error.__class__](error_msg)

resp_map = {
ValidationError: HttpResponseBadRequest,
PermissionDenied: HttpResponseForbidden,
ParseError: HttpResponseBadRequest,
}
return resp_map[error.__class__](error.message)

status_code = resp_map[error.__class__].status_code
return JsonResponse(error_msg, status=status_code)


def dsv(spec, multirow=False):
Expand Down
36 changes: 33 additions & 3 deletions test/test_decorator_dj.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from django.conf import settings
from django.core.handlers.asgi import ASGIRequest
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse, HttpResponseBadRequest
from django.http import HttpResponse, JsonResponse
from django.test import RequestFactory
from django.views import View

Expand Down Expand Up @@ -58,7 +58,7 @@ class _ViewSpec:
class _View(View):
@dsv(_ViewSpec)
def decorated_func(self, request, named_arg):
pass
return HttpResponse(status=200)

factory = RequestFactory()
wsgi_req = factory.request()
Expand All @@ -68,7 +68,8 @@ def decorated_func(self, request, named_arg):
view.decorated_func(wsgi_req, named_arg='1') # should pass validation

resp = view.decorated_func(wsgi_req, named_arg='')
assert isinstance(resp, HttpResponseBadRequest)
self.assertIsInstance(resp, JsonResponse)
self.assertTrue(resp.status_code, 400)

def test_data_and_url_params_should_not_have_intersection(self):
# arrange
Expand Down Expand Up @@ -220,6 +221,35 @@ def decorated_func(self, request, field_a):
with self.assertRaises(Exception):
non_view.decorated_func(fake_args, field_a='1')

def test_json_response_content(self):
# arrange
class _ViewSpec:
named_arg = Checker([DIGIT_STR])

class _View(View):
@dsv(_ViewSpec)
def decorated_func(self, request, named_arg):
return HttpResponse(status=200)

factory = RequestFactory()
req = factory.request()
view = _View()

# action
resp_valid = view.decorated_func(req, named_arg='1')
resp_invalid = view.decorated_func(req, named_arg='hi')

# assert
self.assertIsInstance(resp_valid, HttpResponse)
self.assertEqual(resp_valid.status_code, 200)

self.assertIsInstance(resp_invalid, JsonResponse)
self.assertEqual(resp_invalid.status_code, 400)
self.assertEqual(
json.loads(resp_invalid.content),
{'messages': ["field: _ViewSpec.named_arg, reason: 'hi' is not a digit str"]},
)


if __name__ == '__main__':
unittest.main()
23 changes: 23 additions & 0 deletions test/test_decorator_drf.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,29 @@ def decorated_func(self, request, field_a):
with self.assertRaises(Exception):
non_view.decorated_func(fake_args, field_a='1')

def test_json_response_content(self):
# arrange
class _ViewSpec:
field_a = Checker([DIGIT_STR])

class _View(View):
@dsv(_ViewSpec)
def decorated_func(self, request, field_a):
pass

factory = RequestFactory()
wsgi_req = factory.request()
req = Request(wsgi_req)
view = _View()

# action & assert
with self.assertRaises(Exception) as exc_info:
view.decorated_func(req, field_a='hi')

self.assertEqual(
exc_info.exception.detail, {'messages': ["field: _ViewSpec.field_a, reason: 'hi' is not a digit str"]}
)


if __name__ == '__main__':
unittest.main()
15 changes: 13 additions & 2 deletions test/test_nested_spec.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import unittest

from data_spec_validator.spec import BOOL, DICT, DIGIT_STR, FLOAT, INT, NONE, SPEC, STR, Checker, validate_data_spec, \
dsv_feature
from data_spec_validator.spec import (
BOOL,
DICT,
DIGIT_STR,
FLOAT,
INT,
NONE,
SPEC,
STR,
Checker,
dsv_feature,
validate_data_spec,
)

from .utils import is_something_error

Expand Down

0 comments on commit 50a2ac1

Please sign in to comment.