Source code for ripozo.adapters.jsonapi
"""
Contains the adapter for the
`JSON API <http://jsonapi.org/format/>`_.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
import six
from ripozo import ResourceBase
from ripozo.adapters.base import AdapterBase
from ripozo.exceptions import JSONAPIFormatException
from ripozo.resources.constructor import ResourceMetaClass
from ripozo.utilities import join_url_parts
_CONTENT_TYPE = 'application/vnd.api+json'
[docs]class JSONAPIAdapter(AdapterBase):
"""
An adapter for formatting and ResourceBase
instance in an appropriate format. See the
`specification <http://jsonapi.org/format/>`_
for more details on what format it will return.
"""
formats = [_CONTENT_TYPE]
extra_headers = {'Content-Type': _CONTENT_TYPE}
@property
def formatted_body(self):
"""
Returns a string in the
`JSON API format. <http://jsonapi.org/format/>`_
:return: The appropriately formatted string
:rtype: unicode|str
"""
data = self._construct_data(self.resource, embedded=True)
return json.dumps(dict(data=data))
def _construct_data(self, resource, embedded=True):
"""
Constructs a resource object according to this
`part of the specification <http://jsonapi.org/format/#document-resource-objects>`_
:param ResourceBase resource: The resource to format
:param bool embedded: A flag to indicate whether
all of the data should be included.
:return: A dictionary representing the resource
according to the specification
:rtype: dict
"""
id_ = self._construct_id(resource)
data = dict(id=id_, type=resource.resource_name)
if embedded:
data['relationships'] = self._construct_relationships(resource)
data['links'] = self._construct_links(resource)
data['attributes'] = resource.properties
else:
data['links'] = {'self': self.combine_base_url_with_resource_url(resource.url)}
return data
def _construct_links(self, resource):
"""
Constructs the links object according
to `this section <http://jsonapi.org/format/#document-links>`_
:param ResourceBase resource: The links from this resource
instance will be used to construct the links object.
:return: A dictionary representing the links object
:rtype: dict
"""
self_url = self.combine_base_url_with_resource_url(resource.url)
links = {'self': self_url}
for link, name, embedded in resource.linked_resources:
links[name] = self.combine_base_url_with_resource_url(link.url)
return links
@staticmethod
def _construct_id(resource):
"""
Constructs a JSON API compatible id.
May not work particularly well for composite ids since
apparently JSON API hates them. It will simply
join the pks with a `"/"` in the appropriate order.
Additionally, for resources with no_pks=True, it
will return an empty string.
:param ResourceBase resource: The resource whose
id needs to be constructed.
:return: The id in a format `<pk1>/<pk2>`
:rtype: unicode
"""
if resource.no_pks:
return ""
pks = resource.pks
id_parts = [resource.item_pks[pk] for pk in pks]
id_ = join_url_parts(*id_parts)
return id_
def _construct_relationships(self, resource):
"""
Constructs the relationships according to the
`specification <http://jsonapi.org/format/#document-resource-object-relationships>`_
:param ResourceBase resource: This is the resource
that the relationships will be constructed for.
It will user the `related_resources` attribute
on the resource to construct the resources
:return: The dictionary representing the relationships
in the appropriate format.
:rtype: dict
"""
# TODO docs
relationships = dict()
for resource, name, embedded in resource.related_resources:
if name not in relationships:
relationships[name] = dict(data=[])
data = relationships[name]['data']
if isinstance(resource, ResourceBase):
data.append(self._construct_data(resource, embedded=embedded))
else:
for res in resource:
data.append(self._construct_data(res, embedded=embedded))
return relationships
@classmethod
@classmethod
@staticmethod
def _parse_id(id_, resource_name):
if resource_name not in ResourceMetaClass.registered_resource_names_map:
raise JSONAPIFormatException('The resource "{0}" is not a valid type'.format(resource_name))
resource_class = ResourceMetaClass.registered_resource_names_map[resource_name]
ids = id_.split('/')
if len(ids) != len(resource_class.pks):
raise JSONAPIFormatException("Unsatisfactory id. There are an unequal number"
"of pks among the ids {0}".format(ids))
return dict(zip(resource_class.pks, ids))