Skip to content

Commit

Permalink
Merge pull request #9 from jackie-greenbaum/add-lid-support
Browse files Browse the repository at this point in the history
Add lid support
  • Loading branch information
jokiefer authored Nov 7, 2024
2 parents 8d3ee50 + 13bdc72 commit e9e2494
Show file tree
Hide file tree
Showing 4 changed files with 615 additions and 13 deletions.
32 changes: 24 additions & 8 deletions atomic_operations/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,24 @@ class AtomicOperationParser(JSONParser):
renderer_class = renderers.JSONRenderer

def check_resource_identifier_object(self, idx: int, resource_identifier_object: Dict, operation_code: str):
if operation_code in ["update", "remove"] and not resource_identifier_object.get("id"):
raise JsonApiParseError(
id="missing-id",
detail="The resource identifier object must contain an `id` member",
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/{'data' if operation_code == 'update' else 'ref'}"
)
if operation_code in ["update", "remove"]:
resource_id = resource_identifier_object.get("id")
resource_lid = resource_identifier_object.get("lid")

if not (resource_id or resource_lid):
raise JsonApiParseError(
id="missing-id",
detail="The resource identifier object must contain an `id` member or a `lid` member",
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/{'data' if operation_code == 'update' else 'ref'}"
)

if resource_id and resource_lid:
raise JsonApiParseError(
id="multiple-id-fields",
detail="Only one of `id`, `lid` may be specified",
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/{'data' if operation_code == 'update' else 'ref'}"
)

if not resource_identifier_object.get("type"):
raise JsonApiParseError(
id="missing-type",
Expand Down Expand Up @@ -150,10 +162,14 @@ def check_operation(self, idx: int, operation: Dict):
pointer=f"/{ATOMIC_OPERATIONS}/{idx}/op"
)

def parse_id_and_type(self, resource_identifier_object):
def parse_id_lid_and_type(self, resource_identifier_object):
parsed_data = {"id": resource_identifier_object.get(
"id")} if "id" in resource_identifier_object else {}
parsed_data["type"] = resource_identifier_object.get("type")

if lid := resource_identifier_object.get("lid", None):
parsed_data["lid"] = lid

return parsed_data

def check_root(self, result):
Expand All @@ -173,7 +189,7 @@ def check_root(self, result):
)

def parse_operation(self, resource_identifier_object, result):
_parsed_data = self.parse_id_and_type(resource_identifier_object)
_parsed_data = self.parse_id_lid_and_type(resource_identifier_object)
_parsed_data.update(self.parse_attributes(resource_identifier_object))
_parsed_data.update(self.parse_relationships(resource_identifier_object))
_parsed_data.update(self.parse_metadata(result))
Expand Down
43 changes: 43 additions & 0 deletions atomic_operations/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Dict, List
from collections import defaultdict

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

lid_to_id = defaultdict(dict)

# TODO: proof how to check permissions for all operations
# permission_classes = TODO
# call def check_permissions for `add` operation
Expand Down Expand Up @@ -94,8 +97,15 @@ def post(self, request, *args, **kwargs):

def handle_sequential(self, serializer, operation_code):
if operation_code in ["add", "update", "update-relationship"]:
lid = serializer.initial_data.get("lid", None)

serializer.is_valid(raise_exception=True)
serializer.save()

if operation_code == "add" and lid:
resource_type = serializer.initial_data["type"]
self.lid_to_id[resource_type][lid] = serializer.data["id"]

if operation_code != "update-relationship":
self.response_data.append(serializer.data)
else:
Expand Down Expand Up @@ -139,6 +149,36 @@ def handle_bulk(self, serializer, current_operation_code, bulk_operation_data):
bulk_operation_data["serializer_collection"][0], current_operation_code)
bulk_operation_data["serializer_collection"] = []

def substitute_lids(self, data, idx, should_raise_unknown_lid_error):
if not isinstance(data, dict):
return

try:
lid = data.get("lid", None)
if lid:
resource_type = data["type"]
data["id"] = self.lid_to_id[resource_type][lid]
except KeyError:
if should_raise_unknown_lid_error:
raise UnprocessableEntity([
{
"id": "unknown-lid",
"detail": f'Object with lid `{lid}` received for operation with index `{idx}` does not exist',
"source": {
"pointer": f"/{ATOMIC_OPERATIONS}/{idx}/data/lid"
},
"status": "422"
}
])

for _, value in data.items():
if isinstance(value, dict):
self.substitute_lids(value, idx, should_raise_unknown_lid_error=True)
elif isinstance(value, list):
[self.substitute_lids(value, idx, should_raise_unknown_lid_error=True) for value in value]

return data

def perform_operations(self, parsed_operations: List[Dict]):
self.response_data = [] # reset local response data storage

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

should_raise_unknown_lid_error = operation_code != "add"
self.substitute_lids(obj, idx, should_raise_unknown_lid_error)

serializer = self.get_serializer(
idx=idx,
data=obj,
Expand Down
100 changes: 96 additions & 4 deletions tests/test_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def test_using_href(self):
}
)

def test_primary_data_without_id(self):
def test_primary_data_without_id_or_lid(self):
data = {
ATOMIC_OPERATIONS: [
{
Expand All @@ -242,7 +242,7 @@ def test_primary_data_without_id(self):
stream = BytesIO(json.dumps(data).encode("utf-8"))
self.assertRaisesRegex(
JsonApiParseError,
"The resource identifier object must contain an `id` member",
"The resource identifier object must contain an `id` member or a `lid` member",
self.parser.parse,
**{
"stream": stream,
Expand All @@ -263,7 +263,7 @@ def test_primary_data_without_id(self):
stream = BytesIO(json.dumps(data).encode("utf-8"))
self.assertRaisesRegex(
JsonApiParseError,
"The resource identifier object must contain an `id` member",
"The resource identifier object must contain an `id` member or a `lid` member",
self.parser.parse,
**{
"stream": stream,
Expand All @@ -284,7 +284,7 @@ def test_primary_data_without_id(self):
stream = BytesIO(json.dumps(data).encode("utf-8"))
self.assertRaisesRegex(
JsonApiParseError,
"The resource identifier object must contain an `id` member",
"The resource identifier object must contain an `id` member or a `lid` member",
self.parser.parse,
**{
"stream": stream,
Expand Down Expand Up @@ -365,3 +365,95 @@ def test_is_atomic_operations(self):
"parser_context": self.parser_context
}
)

def test_parse_with_lid(self):
data = {
ATOMIC_OPERATIONS: [
{
"op": "add",
"data": {
"lid": "1",
"type": "articles",
"attributes": {
"title": "JSON API paints my bikeshed!"
}
}
},
{
"op": "update",
"data": {
"lid": "1",
"type": "articles",
"attributes": {
"title": "JSON API supports lids!"
}
}
},
{
"op": "remove",
"ref": {
"lid": "1",
"type": "articles",
}
}
]
}
stream = BytesIO(json.dumps(data).encode("utf-8"))

result = self.parser.parse(stream, parser_context=self.parser_context)
expected_result = [
{
"add": {
"type": "articles",
"lid": "1",
"title": "JSON API paints my bikeshed!"
}
},
{
"update": {
"lid": "1",
"type": "articles",
"title": "JSON API supports lids!"
}
},
{
"remove": {
"lid": "1",
"type": "articles"
}
}
]
self.assertEqual(expected_result, result)

def test_primary_data_with_id_and_lid(self):
data = {
ATOMIC_OPERATIONS: [
{
"op": "add",
"data": {
"lid": "1",
"type": "articles",
"title": "JSON API paints my bikeshed!"
}
},
{
"op": "update",
"data": {
"lid": "1",
"id": "1",
"type": "articles",
"title": "JSON API supports lids!"
}
}
]
}
stream = BytesIO(json.dumps(data).encode("utf-8"))
self.assertRaisesRegex(
JsonApiParseError,
"Only one of `id`, `lid` may be specified",
self.parser.parse,
**{
"stream": stream,
"parser_context": self.parser_context
}
)
Loading

0 comments on commit e9e2494

Please sign in to comment.