forked from django-json-api/django-rest-framework-json-api
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathparsers.py
172 lines (148 loc) · 6.55 KB
/
parsers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""
Parsers
"""
from rest_framework import parsers
from rest_framework.exceptions import ParseError
from rest_framework_json_api import exceptions, renderers, serializers
from rest_framework_json_api.utils import get_resource_name, undo_format_field_names
class JSONParser(parsers.JSONParser):
"""
Similar to `JSONRenderer`, the `JSONParser` you may override the following methods if you
need highly custom parsing control.
A JSON:API client will send a payload that looks like this:
.. code:: json
{
"data": {
"type": "identities",
"id": 1,
"attributes": {
"first_name": "John",
"last_name": "Coltrane"
}
}
}
We extract the attributes so that DRF serializers can work as normal.
"""
media_type = "application/vnd.api+json"
renderer_class = renderers.JSONRenderer
@staticmethod
def parse_attributes(data):
attributes = data.get("attributes") or dict()
return undo_format_field_names(attributes)
@staticmethod
def parse_relationships(data):
relationships = data.get("relationships") or dict()
relationships = undo_format_field_names(relationships)
# Parse the relationships
parsed_relationships = dict()
for field_name, field_data in relationships.items():
field_data = field_data.get("data")
if isinstance(field_data, dict) or field_data is None:
parsed_relationships[field_name] = field_data
elif isinstance(field_data, list):
parsed_relationships[field_name] = list(
relation for relation in field_data
)
return parsed_relationships
@staticmethod
def parse_metadata(result):
"""
Returns a dictionary which will be merged into parsed data of the request. By default,
it reads the `meta` content in the request body and returns it in a dictionary with
a `_meta` top level key.
"""
metadata = result.get("meta")
if metadata:
return {"_meta": metadata}
else:
return {}
def parse(self, stream, media_type=None, parser_context=None):
"""
Parses the incoming bytestream as JSON and returns the resulting data
"""
result = super(JSONParser, self).parse(
stream, media_type=media_type, parser_context=parser_context
)
if not isinstance(result, dict) or "data" not in result:
raise ParseError("Received document does not contain primary data")
data = result.get("data")
parser_context = parser_context or {}
view = parser_context.get("view")
from rest_framework_json_api.views import RelationshipView
if isinstance(view, RelationshipView):
# We skip parsing the object as JSON:API Resource Identifier Object and not a regular
# Resource Object
if isinstance(data, list):
for resource_identifier_object in data:
if not (
resource_identifier_object.get("id")
and resource_identifier_object.get("type")
):
raise ParseError(
"Received data contains one or more malformed JSON:API "
"Resource Identifier Object(s)"
)
elif not (data.get("id") and data.get("type")):
raise ParseError(
"Received data is not a valid JSON:API Resource Identifier Object"
)
return data
request = parser_context.get("request")
method = request and request.method
# Sanity check
if not isinstance(data, dict):
raise ParseError(
"Received data is not a valid JSON:API Resource Identifier Object"
)
# Check for inconsistencies
if method in ("PUT", "POST", "PATCH"):
resource_name = get_resource_name(
parser_context, expand_polymorphic_types=True
)
if isinstance(resource_name, str):
if data.get("type") != resource_name:
raise exceptions.Conflict(
"The resource object's type ({data_type}) is not the type that "
"constitute the collection represented by the endpoint "
"({resource_type}).".format(
data_type=data.get("type"), resource_type=resource_name
)
)
else:
if data.get("type") not in resource_name:
raise exceptions.Conflict(
"The resource object's type ({data_type}) is not the type that "
"constitute the collection represented by the endpoint "
"(one of [{resource_types}]).".format(
data_type=data.get("type"),
resource_types=", ".join(resource_name),
)
)
if not data.get("id") and method in ("PATCH", "PUT"):
raise ParseError(
"The resource identifier object must contain an 'id' member"
)
if method in ("PATCH", "PUT"):
lookup_url_kwarg = getattr(view, "lookup_url_kwarg", None) or getattr(
view, "lookup_field", None
)
if lookup_url_kwarg and str(data.get("id")) != str(
view.kwargs[lookup_url_kwarg]
):
raise exceptions.Conflict(
"The resource object's id ({data_id}) does not match url's "
"lookup id ({url_id})".format(
data_id=data.get("id"), url_id=view.kwargs[lookup_url_kwarg]
)
)
# Construct the return data
serializer_class = getattr(view, "serializer_class", None)
parsed_data = {"id": data.get("id")} if "id" in data else {}
# `type` field needs to be allowed in none polymorphic serializers
if serializer_class is not None:
if issubclass(serializer_class, serializers.PolymorphicModelSerializer):
parsed_data["type"] = data.get("type")
parsed_data.update(self.parse_attributes(data))
parsed_data.update(self.parse_relationships(data))
parsed_data.update(self.parse_metadata(result))
return parsed_data