Skip to content

Commit

Permalink
Merge pull request #18 from Wesmania/fix-one-to-one-null-relation
Browse files Browse the repository at this point in the history
Fix handling null one-to-one-relationship
  • Loading branch information
ajjn authored Jul 16, 2018
2 parents d6c4947 + f19c59d commit c5e7156
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 31 deletions.
29 changes: 24 additions & 5 deletions src/jsonapi_client/relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,21 +223,30 @@ class SingleRelationship(AbstractRelationship):
"""
def _handle_data(self, data):
super()._handle_data(data)
self._resource_identifier = ResourceIdentifier(self.session, self._resource_data)
if self._resource_data is None:
self._resource_identifier = None
else:
self._resource_identifier = ResourceIdentifier(self.session, self._resource_data)
del self._resource_data # This is not intended to be used after this

async def _fetch_async(self) -> 'List[ResourceObject]':
self.session.assert_async()
res_id = self._resource_identifier
res = await self.session.fetch_resource_by_resource_identifier_async(res_id)
self._resources = {(res.type, res.id): res}
if res_id is None:
self._resources = {None: None}
else:
res = await self.session.fetch_resource_by_resource_identifier_async(res_id)
self._resources = {(res.type, res.id): res}
return list(self._resources.values())

def _fetch_sync(self) -> 'List[ResourceObject]':
self.session.assert_sync()
res_id = self._resource_identifier
res = self.session.fetch_resource_by_resource_identifier(res_id)
self._resources = {(res.type, res.id): res}
if res_id is None:
self._resources = {None: None}
else:
res = self.session.fetch_resource_by_resource_identifier(res_id)
self._resources = {(res.type, res.id): res}
return list(self._resources.values())

def __bool__(self):
Expand All @@ -252,12 +261,22 @@ def is_single(self) -> bool:

@property
def url(self) -> str:
if self._resource_identifier is None:
return self.links.related
return self._resource_identifier.url

@property
def as_json_resource_identifiers(self) -> dict:
if self._resource_identifier is None:
return None
return self._resource_identifier.as_resource_identifier_dict()

def _value_to_identifier(self, value: R_IDENT_TYPES, type_: str='') \
-> 'Union[ResourceIdentifier, ResourceObject]':
if value is None:
return None
return super()._value_to_identifier(value, type_)

def set(self, new_value: R_IDENT_TYPES, type_: str='') -> None:

self._resource_identifier = self._value_to_identifier(new_value, type_)
Expand Down
21 changes: 12 additions & 9 deletions src/jsonapi_client/resourceobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,18 @@ def _determine_class(self, data: dict, relation_type: str=None):
:param relation_type: either 'to-one' or 'to-many'
"""
from . import relationships as rel
relationship_data = data.get('data')
if isinstance(relationship_data, list):
if not (not relation_type or relation_type == RelationType.TO_MANY):
logger.error('Conflicting information about relationship')
return rel.MultiRelationship
elif relationship_data:
if not(not relation_type or relation_type == RelationType.TO_ONE):
logger.error('Conflicting information about relationship')
return rel.SingleRelationship
if 'data' in data:
relationship_data = data['data']
if isinstance(relationship_data, list):
if not (not relation_type or relation_type == RelationType.TO_MANY):
logger.error('Conflicting information about relationship')
return rel.MultiRelationship
elif relationship_data is None or isinstance(relationship_data, dict):
if not(not relation_type or relation_type == RelationType.TO_ONE):
logger.error('Conflicting information about relationship')
return rel.SingleRelationship
else:
raise ValidationError('Relationship data key is invalid')
elif 'links' in data:
return rel.LinkRelationship
elif 'meta' in data:
Expand Down
29 changes: 29 additions & 0 deletions tests/json/articles.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@
"links": {
"self": "http://example.com/articles/2"
}
},
{
"type": "articles",
"id": "3",
"attributes": {
"title": "An authorless book!",
"nested1": {"nested": {"name": "test"}}
},
"relationships": {
"author": {
"data": null
},
"comments": {
"links": {
"self": "http://example.com/articles/3/relationships/comments",
"related": "http://example.com/articles/3/comments"
},
"data": []
},
"comment-or-author": {
"data": null
},
"comments-or-authors": {
"data": []
}
},
"links": {
"self": "http://example.com/articles/2"
}
}
],
"included": [{
Expand Down
61 changes: 44 additions & 17 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ async def test_initialization_async(mocked_fetch, article_schema):
def test_basic_attributes(mocked_fetch, article_schema):
s = Session('http://localhost:8080', schema=article_schema)
doc = s.get('articles')
assert len(doc.resources) == 2
assert len(doc.resources) == 3
article = doc.resources[0]
assert article.id == "1"
assert article.type == "articles"
Expand All @@ -253,7 +253,7 @@ def test_basic_attributes(mocked_fetch, article_schema):
async def test_basic_attributes_async(mocked_fetch, article_schema):
s = Session('http://localhost:8080', enable_async=True, schema=article_schema)
doc = await s.get('articles')
assert len(doc.resources) == 2
assert len(doc.resources) == 3
article = doc.resources[0]
assert article.id == "1"
assert article.type == "articles"
Expand All @@ -272,7 +272,7 @@ async def test_basic_attributes_async(mocked_fetch, article_schema):

def test_relationships_single(mocked_fetch, article_schema):
s = Session('http://localhost:8080', schema=article_schema)
article, article2 = s.get('articles').resources
article, article2, article3 = s.get('articles').resources
author = article.author
assert {i for i in dir(author.fields) if not i.startswith('_')} \
== {'first_name', 'last_name', 'twitter'}
Expand All @@ -295,14 +295,17 @@ def test_relationships_single(mocked_fetch, article_schema):

assert article2.comment_or_author.id == '9'
assert article2.comment_or_author.type == 'people'
assert article2.comment_or_author.first_name == 'Dan' \
assert article2.comment_or_author.first_name == 'Dan'

assert article3.author is None
assert article3.comment_or_author is None


@pytest.mark.asyncio
async def test_relationships_iterator_async(mocked_fetch, article_schema):
s = Session('http://localhost:8080', enable_async=True, schema=article_schema, use_relationship_iterator=True)
doc = await s.get('articles')
article, article2 = doc.resources
article, article2, article3 = doc.resources
comments = article.comments
assert isinstance(comments, jsonapi_client.relationships.MultiRelationship)
assert len(comments._resource_identifiers) == 2
Expand All @@ -312,7 +315,7 @@ async def test_relationships_iterator_async(mocked_fetch, article_schema):
async def test_relationships_single_async(mocked_fetch, article_schema):
s = Session('http://localhost:8080', enable_async=True, schema=article_schema)
doc = await s.get('articles')
article, article2 = doc.resources
article, article2, article3 = doc.resources

author = article.author
assert isinstance(author, jsonapi_client.relationships.SingleRelationship)
Expand Down Expand Up @@ -345,11 +348,16 @@ async def test_relationships_single_async(mocked_fetch, article_schema):
assert article2.comment_or_author.resource.id == '9'
assert article2.comment_or_author.resource.type == 'people'
assert article2.comment_or_author.resource.first_name == 'Dan'

await article3.author.fetch()
await article3.comment_or_author.fetch()
assert article3.author.resource is None
assert article3.comment_or_author.resource is None
s.close()

def test_relationships_multi(mocked_fetch, article_schema):
s = Session('http://localhost:8080', schema=article_schema)
article, article2 = s.get('articles').resources
article, article2, article3 = s.get('articles').resources
comments = article.comments
assert len(comments) == 2
c1, c2 = comments
Expand Down Expand Up @@ -424,11 +432,11 @@ def test_fetch_external_resources(mocked_fetch, article_schema):
session = article.session
c1, c2 = comments
assert c1.body == "First!"
assert len(session.resources_by_resource_identifier) == 5
assert len(session.resources_by_resource_identifier) == 6
assert len(session.resources_by_link) == 5
assert len(session.documents_by_link) == 1
assert c1.author.id == "2"
assert len(session.resources_by_resource_identifier) == 6
assert len(session.resources_by_resource_identifier) == 7
assert len(session.resources_by_link) == 6
assert len(session.documents_by_link) == 2

Expand All @@ -449,7 +457,7 @@ async def test_fetch_external_resources_async(mocked_fetch, article_schema):
session = article.session
c1, c2 = await comments.fetch()
assert c1.body == "First!"
assert len(session.resources_by_resource_identifier) == 5
assert len(session.resources_by_resource_identifier) == 6
assert len(session.resources_by_link) == 5
assert len(session.documents_by_link) == 1

Expand All @@ -459,7 +467,7 @@ async def test_fetch_external_resources_async(mocked_fetch, article_schema):
# fetch external content
c1_author = c1.author.resource
assert c1_author.id == "2"
assert len(session.resources_by_resource_identifier) == 6
assert len(session.resources_by_resource_identifier) == 7
assert len(session.resources_by_link) == 6
assert len(session.documents_by_link) == 2

Expand Down Expand Up @@ -947,6 +955,8 @@ def make_patch_json(ids, type_, field_name=None):
content = {'data': [{'id': str(i), 'type': str(j)} for i, j in ids]}
else:
content = {'data': [{'id': str(i), 'type': type_} for i in ids]}
elif ids is None:
content = {'data': None}
else:
content = {'data': {'id': str(ids), 'type': type_}}

Expand Down Expand Up @@ -1122,6 +1132,21 @@ def test_posting_relationships(mock_req, article_schema):
a.commit()


def test_posting_with_null_to_one_relationship(mock_req, article_schema):
if not article_schema:
return

s = Session('http://localhost:8080/', schema=article_schema)
a = s.create('articles',
title='Test article',
comments=[],
author=None,
comments_or_authors=[]
)
with mock.patch('jsonapi_client.session.Session.read'):
a.commit()


def test_posting_successfull_without_schema(mock_req):
s = Session('http://localhost:80801/api')
a = s.create('leases')
Expand Down Expand Up @@ -1183,7 +1208,7 @@ def test_posting_post_validation_error():

def test_relationship_manipulation(mock_req, article_schema, mocked_fetch, mock_update_resource):
s = Session('http://localhost:80801/', schema=article_schema)
article, article2 = s.get('articles').resources
article, article2, article3 = s.get('articles').resources
assert article.relationships.author.resource.id == '9'
if article_schema:
assert article.relationships.author.type == 'people'
Expand Down Expand Up @@ -1290,6 +1315,13 @@ def test_relationship_manipulation(mock_req, article_schema, mocked_fetch, mock_
mock_req.assert_called_once_with('patch', 'http://example.com/articles/1',
make_patch_json([('5', 'comments'), ('2', 'people')], None, 'comments-or-authors'))

mock_req.reset_mock()
article.relationships.author.set(None)

article.commit()
mock_req.assert_called_once_with('patch', 'http://example.com/articles/1',
make_patch_json(None, None, 'author'))


@pytest.mark.asyncio
async def test_relationship_manipulation_async(mock_req_async, mocked_fetch, article_schema, mock_update_resource):
Expand Down Expand Up @@ -1458,8 +1490,3 @@ def test_relationship_manipulation_alternative_api(mock_req, mocked_fetch, artic
mock_req.reset_mock()

#assert article.relationships.comments.value == ['7', '6']





0 comments on commit c5e7156

Please sign in to comment.