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 import codecs
from urllib import parse from urllib import parse
import jsonpatch
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict from django.http import QueryDict
@ -67,6 +68,33 @@ class JSONParser(BaseParser):
raise ParseError('JSON parse error - %s' % str(exc)) 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): class FormParser(BaseParser):
""" """
Parser for form data. 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 import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from jsonpatch import JsonPatch
from rest_framework.compat import postgres_fields from rest_framework.compat import postgres_fields
from rest_framework.exceptions import ErrorDetail, ValidationError 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, ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs,
get_relation_kwargs, get_url_kwargs get_relation_kwargs, get_url_kwargs
) )
from rest_framework.utils.json_patch import apply_json_patch
from rest_framework.utils.serializer_helpers import ( from rest_framework.utils.serializer_helpers import (
BindingDict, BoundField, JSONBoundField, NestedBoundField, ReturnDict, BindingDict, BoundField, JSONBoundField, NestedBoundField, ReturnDict,
ReturnList ReturnList
@ -108,6 +110,12 @@ class BaseSerializer(Field):
def __init__(self, instance=None, data=empty, **kwargs): def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance 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: if data is not empty:
self.initial_data = data self.initial_data = data
self.partial = kwargs.pop('partial', False) self.partial = kwargs.pop('partial', False)

View File

@ -32,6 +32,7 @@ DEFAULTS = {
], ],
'DEFAULT_PARSER_CLASSES': [ 'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser', 'rest_framework.parsers.JSONParser',
'rest_framework.parsers.JSONPatchParser',
'rest_framework.parsers.FormParser', 'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser' '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}')