mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-27 08:29:59 +03:00
Handle json_patch (rfc 6902)
This commit is contained in:
parent
580bf45ccf
commit
f8813b89f4
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
],
|
],
|
||||||
|
|
53
rest_framework/utils/json_patch.py
Normal file
53
rest_framework/utils/json_patch.py
Normal 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}')
|
Loading…
Reference in New Issue
Block a user