Skip to content

Commit e9e2494

Browse files
authored
Merge pull request #9 from jackie-greenbaum/add-lid-support
Add lid support
2 parents 8d3ee50 + 13bdc72 commit e9e2494

File tree

4 files changed

+615
-13
lines changed

4 files changed

+615
-13
lines changed

atomic_operations/parsers.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,24 @@ class AtomicOperationParser(JSONParser):
4343
renderer_class = renderers.JSONRenderer
4444

4545
def check_resource_identifier_object(self, idx: int, resource_identifier_object: Dict, operation_code: str):
46-
if operation_code in ["update", "remove"] and not resource_identifier_object.get("id"):
47-
raise JsonApiParseError(
48-
id="missing-id",
49-
detail="The resource identifier object must contain an `id` member",
50-
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/{'data' if operation_code == 'update' else 'ref'}"
51-
)
46+
if operation_code in ["update", "remove"]:
47+
resource_id = resource_identifier_object.get("id")
48+
resource_lid = resource_identifier_object.get("lid")
49+
50+
if not (resource_id or resource_lid):
51+
raise JsonApiParseError(
52+
id="missing-id",
53+
detail="The resource identifier object must contain an `id` member or a `lid` member",
54+
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/{'data' if operation_code == 'update' else 'ref'}"
55+
)
56+
57+
if resource_id and resource_lid:
58+
raise JsonApiParseError(
59+
id="multiple-id-fields",
60+
detail="Only one of `id`, `lid` may be specified",
61+
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/{'data' if operation_code == 'update' else 'ref'}"
62+
)
63+
5264
if not resource_identifier_object.get("type"):
5365
raise JsonApiParseError(
5466
id="missing-type",
@@ -150,10 +162,14 @@ def check_operation(self, idx: int, operation: Dict):
150162
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/op"
151163
)
152164

153-
def parse_id_and_type(self, resource_identifier_object):
165+
def parse_id_lid_and_type(self, resource_identifier_object):
154166
parsed_data = {"id": resource_identifier_object.get(
155167
"id")} if "id" in resource_identifier_object else {}
156168
parsed_data["type"] = resource_identifier_object.get("type")
169+
170+
if lid := resource_identifier_object.get("lid", None):
171+
parsed_data["lid"] = lid
172+
157173
return parsed_data
158174

159175
def check_root(self, result):
@@ -173,7 +189,7 @@ def check_root(self, result):
173189
)
174190

175191
def parse_operation(self, resource_identifier_object, result):
176-
_parsed_data = self.parse_id_and_type(resource_identifier_object)
192+
_parsed_data = self.parse_id_lid_and_type(resource_identifier_object)
177193
_parsed_data.update(self.parse_attributes(resource_identifier_object))
178194
_parsed_data.update(self.parse_relationships(resource_identifier_object))
179195
_parsed_data.update(self.parse_metadata(result))

atomic_operations/views.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Dict, List
2+
from collections import defaultdict
23

34
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
45
from django.db.transaction import atomic
@@ -27,6 +28,8 @@ class AtomicOperationView(APIView):
2728
sequential = True
2829
response_data: List[Dict] = []
2930

31+
lid_to_id = defaultdict(dict)
32+
3033
# TODO: proof how to check permissions for all operations
3134
# permission_classes = TODO
3235
# call def check_permissions for `add` operation
@@ -94,8 +97,15 @@ def post(self, request, *args, **kwargs):
9497

9598
def handle_sequential(self, serializer, operation_code):
9699
if operation_code in ["add", "update", "update-relationship"]:
100+
lid = serializer.initial_data.get("lid", None)
101+
97102
serializer.is_valid(raise_exception=True)
98103
serializer.save()
104+
105+
if operation_code == "add" and lid:
106+
resource_type = serializer.initial_data["type"]
107+
self.lid_to_id[resource_type][lid] = serializer.data["id"]
108+
99109
if operation_code != "update-relationship":
100110
self.response_data.append(serializer.data)
101111
else:
@@ -139,6 +149,36 @@ def handle_bulk(self, serializer, current_operation_code, bulk_operation_data):
139149
bulk_operation_data["serializer_collection"][0], current_operation_code)
140150
bulk_operation_data["serializer_collection"] = []
141151

152+
def substitute_lids(self, data, idx, should_raise_unknown_lid_error):
153+
if not isinstance(data, dict):
154+
return
155+
156+
try:
157+
lid = data.get("lid", None)
158+
if lid:
159+
resource_type = data["type"]
160+
data["id"] = self.lid_to_id[resource_type][lid]
161+
except KeyError:
162+
if should_raise_unknown_lid_error:
163+
raise UnprocessableEntity([
164+
{
165+
"id": "unknown-lid",
166+
"detail": f'Object with lid `{lid}` received for operation with index `{idx}` does not exist',
167+
"source": {
168+
"pointer": f"/{ATOMIC_OPERATIONS}/{idx}/data/lid"
169+
},
170+
"status": "422"
171+
}
172+
])
173+
174+
for _, value in data.items():
175+
if isinstance(value, dict):
176+
self.substitute_lids(value, idx, should_raise_unknown_lid_error=True)
177+
elif isinstance(value, list):
178+
[self.substitute_lids(value, idx, should_raise_unknown_lid_error=True) for value in value]
179+
180+
return data
181+
142182
def perform_operations(self, parsed_operations: List[Dict]):
143183
self.response_data = [] # reset local response data storage
144184

@@ -154,6 +194,9 @@ def perform_operations(self, parsed_operations: List[Dict]):
154194
operation_code = next(iter(operation))
155195
obj = operation[operation_code]
156196

197+
should_raise_unknown_lid_error = operation_code != "add"
198+
self.substitute_lids(obj, idx, should_raise_unknown_lid_error)
199+
157200
serializer = self.get_serializer(
158201
idx=idx,
159202
data=obj,

tests/test_parsers.py

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def test_using_href(self):
228228
}
229229
)
230230

231-
def test_primary_data_without_id(self):
231+
def test_primary_data_without_id_or_lid(self):
232232
data = {
233233
ATOMIC_OPERATIONS: [
234234
{
@@ -242,7 +242,7 @@ def test_primary_data_without_id(self):
242242
stream = BytesIO(json.dumps(data).encode("utf-8"))
243243
self.assertRaisesRegex(
244244
JsonApiParseError,
245-
"The resource identifier object must contain an `id` member",
245+
"The resource identifier object must contain an `id` member or a `lid` member",
246246
self.parser.parse,
247247
**{
248248
"stream": stream,
@@ -263,7 +263,7 @@ def test_primary_data_without_id(self):
263263
stream = BytesIO(json.dumps(data).encode("utf-8"))
264264
self.assertRaisesRegex(
265265
JsonApiParseError,
266-
"The resource identifier object must contain an `id` member",
266+
"The resource identifier object must contain an `id` member or a `lid` member",
267267
self.parser.parse,
268268
**{
269269
"stream": stream,
@@ -284,7 +284,7 @@ def test_primary_data_without_id(self):
284284
stream = BytesIO(json.dumps(data).encode("utf-8"))
285285
self.assertRaisesRegex(
286286
JsonApiParseError,
287-
"The resource identifier object must contain an `id` member",
287+
"The resource identifier object must contain an `id` member or a `lid` member",
288288
self.parser.parse,
289289
**{
290290
"stream": stream,
@@ -365,3 +365,95 @@ def test_is_atomic_operations(self):
365365
"parser_context": self.parser_context
366366
}
367367
)
368+
369+
def test_parse_with_lid(self):
370+
data = {
371+
ATOMIC_OPERATIONS: [
372+
{
373+
"op": "add",
374+
"data": {
375+
"lid": "1",
376+
"type": "articles",
377+
"attributes": {
378+
"title": "JSON API paints my bikeshed!"
379+
}
380+
}
381+
},
382+
{
383+
"op": "update",
384+
"data": {
385+
"lid": "1",
386+
"type": "articles",
387+
"attributes": {
388+
"title": "JSON API supports lids!"
389+
}
390+
}
391+
},
392+
{
393+
"op": "remove",
394+
"ref": {
395+
"lid": "1",
396+
"type": "articles",
397+
}
398+
}
399+
]
400+
}
401+
stream = BytesIO(json.dumps(data).encode("utf-8"))
402+
403+
result = self.parser.parse(stream, parser_context=self.parser_context)
404+
expected_result = [
405+
{
406+
"add": {
407+
"type": "articles",
408+
"lid": "1",
409+
"title": "JSON API paints my bikeshed!"
410+
}
411+
},
412+
{
413+
"update": {
414+
"lid": "1",
415+
"type": "articles",
416+
"title": "JSON API supports lids!"
417+
}
418+
},
419+
{
420+
"remove": {
421+
"lid": "1",
422+
"type": "articles"
423+
}
424+
}
425+
]
426+
self.assertEqual(expected_result, result)
427+
428+
def test_primary_data_with_id_and_lid(self):
429+
data = {
430+
ATOMIC_OPERATIONS: [
431+
{
432+
"op": "add",
433+
"data": {
434+
"lid": "1",
435+
"type": "articles",
436+
"title": "JSON API paints my bikeshed!"
437+
}
438+
},
439+
{
440+
"op": "update",
441+
"data": {
442+
"lid": "1",
443+
"id": "1",
444+
"type": "articles",
445+
"title": "JSON API supports lids!"
446+
}
447+
}
448+
]
449+
}
450+
stream = BytesIO(json.dumps(data).encode("utf-8"))
451+
self.assertRaisesRegex(
452+
JsonApiParseError,
453+
"Only one of `id`, `lid` may be specified",
454+
self.parser.parse,
455+
**{
456+
"stream": stream,
457+
"parser_context": self.parser_context
458+
}
459+
)

0 commit comments

Comments
 (0)