Handle json_patch (rfc 6902)

This commit is contained in:
Yoann 2021-12-04 15:46:52 +01:00
parent 580bf45ccf
commit f8813b89f4
4 changed files with 90 additions and 0 deletions

View File

@ -7,6 +7,7 @@ on the request, such as form content or json encoded data.
import codecs
from urllib import parse
import jsonpatch
from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict
@ -67,6 +68,33 @@ class JSONParser(BaseParser):
raise ParseError('JSON parse error - %s' % str(exc))
class JSONPatchParser(BaseParser):
"""
Parses PATCH RFC 6902 JSON-serialized data.
"""
media_type = 'application/json-patch+json'
renderer_class = renderers.JSONRenderer
strict = api_settings.STRICT_JSON
def parse(self, stream, media_type=None, parser_context=None):
"""
Parses the incoming bytestream as JSON and returns the resulting data as json patch.
"""
parser_context = parser_context or {}
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
try:
decoded_stream = codecs.getreader(encoding)(stream)
parse_constant = json.strict_constant if self.strict else None
data = json.load(decoded_stream, parse_constant=parse_constant)
return jsonpatch.JsonPatch(data)
except ValueError as exc:
raise ParseError('JSON parse error - %s' % str(exc))
except jsonpatch.InvalidJsonPatch as exc:
raise ParseError('JSON Patch (rfc 6902) invalid - %s' % str(exc))
class FormParser(BaseParser):
"""
Parser for form data.

View File

@ -23,6 +23,7 @@ from django.db.models.fields import Field as DjangoModelField
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from jsonpatch import JsonPatch
from rest_framework.compat import postgres_fields
from rest_framework.exceptions import ErrorDetail, ValidationError
@ -33,6 +34,7 @@ from rest_framework.utils.field_mapping import (
ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs,
get_relation_kwargs, get_url_kwargs
)
from rest_framework.utils.json_patch import apply_json_patch
from rest_framework.utils.serializer_helpers import (
BindingDict, BoundField, JSONBoundField, NestedBoundField, ReturnDict,
ReturnList
@ -108,6 +110,12 @@ class BaseSerializer(Field):
def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance
if isinstance(data, JsonPatch):
# serialise current instance to get a dict
instance_serialized = self.__class__(instance).data
data = apply_json_patch(patch=data, current_state=instance_serialized)
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)

View File

@ -32,6 +32,7 @@ DEFAULTS = {
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.JSONPatchParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
],

View File

@ -0,0 +1,53 @@
from jsonpatch import (
JsonPatch,
InvalidJsonPatch,
JsonPatchConflict,
JsonPatchTestFailed,
JsonPointerException,
)
from rest_framework.exceptions import ValidationError, ParseError
def filter_state(state, paths_parts):
filtered_state = {}
for parts in paths_parts:
if len(parts) > 1:
parts = iter(parts)
next_part = next(parts)
parts = [list(parts)]
filtered_state[next_part] = filter_state(state[next_part], parts)
elif len(parts) == 1:
filtered_state[parts[0]] = state[parts[0]]
else:
# empty parts will raise JsonPointerException during apply()
# this type of error should be checked by json_patch at the
# initilization not during the application.
continue
return filtered_state
def apply_json_patch(patch: JsonPatch, current_state: dict):
field = None
try:
# empty parts will raise JsonPointerException during apply()
# this type of error should be checked by json_patch at the
# initilization not during the application.
paths_parts = [[part for op in patch._ops for part in op.pointer.parts if part]]
filtered_state = filter_state(current_state, paths_parts)
return patch.apply(filtered_state)
except KeyError:
raise ValidationError(
{'details': f'JSON Patch (rfc 6902) path does not exist - {field}'}
)
except JsonPatchConflict as exc:
raise ValidationError({'details': f'JSON Patch (rfc 6902) conflict - {exc}'})
except JsonPatchTestFailed as exc:
raise ValidationError({'details': f'JSON Patch (rfc 6902) test failed - {exc}'})
except JsonPointerException as exc:
raise ValidationError(
{'details': f"JSON Patch (rfc 6902) path's part invalid - {exc}"}
)
except InvalidJsonPatch as exc:
raise ParseError(f'JSON Patch (rfc 6902) invalid - {exc}')