Skip to content

Commit

Permalink
Merge pull request #715 from e0ipso/2.x-formatter-transformer-write-o…
Browse files Browse the repository at this point in the history
…perations-jsonapi

2.x Formatter transformer write operations JSON API
  • Loading branch information
e0ipso committed Nov 2, 2015
2 parents 2a97c0d + 2240903 commit a7b6175
Show file tree
Hide file tree
Showing 15 changed files with 596 additions and 162 deletions.
33 changes: 27 additions & 6 deletions docs/api_drupal.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,18 +198,39 @@ $parsed_body = array(
'tags' => array(
array(
// Create a new term.
'label' => 'child1',
'body' => array(
'label' => 'child1',
),
'request' => array(
'method' => 'POST',
'headers' => array(
'X-CSRF-Token' => 'my-csrf-token',
),
),
),
array(
// PATCH an existing term.
'label' => 'new title by PATCH',
'body' => array(
'label' => 'new title by PATCH',
),
'id' => 12,
'request' => array(
'method' => 'PATCH',
),
),
array(
'__application' => array(
'method' => \Drupal\restful\Http\RequestInterface::METHOD_PUT,
// PATCH an existing term.
'body' => array(
'label' => 'new title by PUT',
),
'id' => 9,
'request' => array(
'method' => 'PUT',
),
// PUT an existing term.
'label' => 'new title by PUT',
),
// Use an existing item.
array(
'id' => 21,
),
),
);
Expand Down
91 changes: 82 additions & 9 deletions docs/api_url.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,76 @@ You can manipulate the resources using different HTTP request types
(e.g. `POST`, `GET`, `DELETE`), HTTP headers, and special query strings
passed in the URL itself.

## Write operations
Write operations can be performed via the POTS (to create items), PUT or PATCH
(to update items) HTTP methods.

## Getting information about the resource
### Basic example
The following request will create an article using the articles resource:

```http
POST /articles HTTP/1.1
Content-Type: application/json
Accept: application/json
{
"title": "My article",
"body": "<p>This is a short one</p>",
"tags": [1, 6, 12]
}
```

Note how we are setting the properties that we want to set using JSON. The
provided payload format needs to match the contents of the `Content-Type` header
(in this case _application/json_).

It's also worth noting that when settings reference fields with multiple values,
you can submit an array of IDs or a string of IDs separated by comas.

### Advanced example
You use sub-requests to manipulate (create or alter) the relationships in a single request.The following example will:

1. Update the title of the article to be _To TDD or Not_.
1. Update the contents of tag 6 to replace it with the provided content.
1. Create a new tag and assign it to the updated article.

```
PATCH /articles/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"title": "To TDD or Not",
"tags": [
{
"id": "6",
"body": {
"label": "Batman!",
"description": "The gadget owner."
},
"request": {
"method": "PATCH"
}
},
{
"body": {
"label": "everything",
"description": "I can only say: 42."
},
"request": {
"method": "POST",
"headers": {"Authorization": "Basic Yoasdkk1="}
}
}
]
}
```

See the
[extension specification](https://gist.github.com/e0ipso/cc95bfce66a5d489bb8a)
for an example using JSON API.

## Getting information about the resource

### Exploring the resource

Expand All @@ -19,9 +86,13 @@ about that resource, in addition to the data itself.
``` shell
curl https://example.com/api/
``
This will output all the available **latest** resources (of course, if you have enabled the "Discovery Resource" option). For example, if there are 3 different api version plugins for content type Article (1.0, 1.1, 2.0) it will display the latest only (2.0 in this case).
This will output all the available **latest** resources (of course, if you have
enabled the "Discovery Resource" option). For example, if there are 3 different
api version plugins for content type Article (1.0, 1.1, 2.0) it will display the
latest only (2.0 in this case).

If you want to display all the versions of all the resources declared add the query **?all=true** like this.
If you want to display all the versions of all the resources declared add the
query **?all=true** like this.

``` shell
curl https://example.com/api?all=true
Expand Down Expand Up @@ -172,12 +243,14 @@ HTTP ``X-CSRF-Token`` header on all writing requests (POST, PUT and DELETE).
You can retrieve the token from ``/api/session/token`` with a standard HTTP
GET request.
See [this](https://github.com/Gizra/angular-restful-auth) AngularJs example that shows a login from a fully decoupled web app
to a Drupal backend.
See [this](https://github.com/Gizra/angular-restful-auth) AngularJs example that
shows a login from a fully decoupled web app to a Drupal backend.
Note: If you use basic auth under .htaccess password you might hit a flood exception, as the server is sending the .htaccess user name and password
as the authentication. In such a case you may set the ``restful_skip_basic_auth`` to TRUE, in order to avoid using it. This will allow
enabling and disabling the basic auth on different environments.
Note: If you use basic auth under .htaccess password you might hit a flood
exception, as the server is sending the .htaccess user name and password as the
authentication. In such a case you may set the ``restful_skip_basic_auth`` to
TRUE, in order to avoid using it. This will allow enabling and disabling the
basic auth on different environments.
```bash
# (Change username and password)
Expand All @@ -192,7 +265,7 @@ curl https://example.com/api/v1.3/articles/1?access_token=YOUR_TOKEN
## Error handling
If an error occurs when operating the REST endpoint via URL, A valid JSON object
with ``code``, ``message`` and ``description`` would be returned.
with ``code``, ``message`` and ``description`` would be returned.
The RESTful module adheres to the [Problem Details for HTTP
APIs](http://tools.ietf.org/html/draft-nottingham-http-problem-06) draft to
Expand Down
19 changes: 12 additions & 7 deletions restful.entity.inc
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,15 @@ function _restful_entity_cache_hashes($entity, $type) {
function restful_entity_update($entity, $type) {
$resource_manager = restful()->getResourceManager();
foreach (_restful_entity_cache_hashes($entity, $type) as $hash) {
$handler = $resource_manager->getPlugin(CacheFragmentController::resourceIdFromHash($hash));
if (!$instance_id = CacheFragmentController::resourceIdFromHash($hash)) {
continue;
}
$handler = $resource_manager->getPlugin($instance_id);
if (!$handler instanceof CacheDecoratedResourceInterface) {
return;
continue;
}
if (!$handler->hasSimpleInvalidation()) {
return;
continue;
}
// You can get away without the fragments for a clear.
$cache_object = new RenderCache(new ArrayCollection(), $hash, $handler->getCacheController());
Expand All @@ -100,13 +103,15 @@ function restful_entity_update($entity, $type) {
function restful_entity_delete($entity, $type) {
$resource_manager = restful()->getResourceManager();
foreach (_restful_entity_cache_hashes($entity, $type) as $hash) {
$handler = $resource_manager->getPlugin(CacheFragmentController::resourceIdFromHash($hash));
if (!$instance_id = CacheFragmentController::resourceIdFromHash($hash)) {
continue;
}
$handler = $resource_manager->getPlugin($instance_id);
if (!$handler instanceof CacheDecoratedResourceInterface) {
// Resource isn't using a cache decorator.
return;
continue;
}
if (!$handler->hasSimpleInvalidation()) {
return;
continue;
}
// You can get away without the fragments for a clear.
$cache_object = new RenderCache(new ArrayCollection(), $hash, $handler->getCacheController());
Expand Down
8 changes: 2 additions & 6 deletions src/Plugin/formatter/FormatterJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,7 @@ protected function addHateoas(array &$data) {
$page = !empty($input['page']) ? $input['page'] : 1;

if ($page > 1) {
$query = array(
'page' => $page - 1,
) + $input;
$query = array('page' => $page - 1) + $input;
$data['previous'] = array(
'title' => 'Previous',
'href' => $resource->versionedUrl('', array('query' => $query), TRUE),
Expand All @@ -171,9 +169,7 @@ protected function addHateoas(array &$data) {
$items_per_page = $resource->getDataProvider()->getRange();
$previous_items = ($page - 1) * $items_per_page;
if (isset($data['count']) && $data['count'] > count($data['data']) + $previous_items) {
$query = array(
'page' => $page + 1,
) + $input;
$query = array('page' => $page + 1) + $input;
$data['next'] = array(
'title' => 'Next',
'href' => $resource->versionedUrl('', array('query' => $query), TRUE),
Expand Down
122 changes: 121 additions & 1 deletion src/Plugin/formatter/FormatterJsonApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Drupal\restful\Plugin\formatter;

use Drupal\restful\Exception\BadRequestException;
use Drupal\restful\Exception\InternalServerErrorException;
use Drupal\restful\Exception\ServerConfigurationException;
use Drupal\restful\Http\Request;
Expand Down Expand Up @@ -404,7 +405,7 @@ protected function embedField(ResourceFieldInterface $resource_field, $parent_id
}
// Check if the resource needs to be included. If not then set 'full_view'
// to false.
$cardinality = $resource_field->cardinality();
$cardinality = $resource_field->getCardinality();
$output = array();
$public_field_name = $resource_field->getPublicName();
if (!$ids = $resource_field->compoundDocumentId($interpreter)) {
Expand Down Expand Up @@ -584,4 +585,123 @@ protected function populateCachePlaceholder(array $field_item, $includes_path) {
);
}

/**
* {@inheritdoc}
*/
public function parseBody($body) {
if (!$decoded_json = drupal_json_decode($body)) {
throw new BadRequestException(sprintf('Invalid JSON provided: %s.', $body));
}
if (empty($decoded_json['data'])) {
throw new BadRequestException(sprintf('Invalid JSON provided: %s.', $body));
}
$data = $decoded_json['data'];
$includes = empty($decoded_json['included']) ? array() : $decoded_json['included'];
// It's always weird to deal with lists of items vs a single item.
$single_item = !ResourceFieldBase::isArrayNumeric($data);
// Make sure we're always dealing with a list of items.
$data = $single_item ? array($data) : $data;
$output = array();
foreach ($data as $item) {
$output[] = $this::restructureItem($item, $includes);
}

return $single_item ? reset($output) : $output;
}

/**
* Take a JSON API item and makes it hierarchical object, like simple JSON.
*
* @param array $item
* The JSON API item.
* @param array $included
* The included pool of elements.
*
* @return array
* The hierarchical object.
*
* @throws \Drupal\restful\Exception\BadRequestException
*/
protected static function restructureItem(array $item, array $included) {
if (empty($item['meta']['subrequest']) && empty($item['attributes']) && empty($item['relationship'])) {
throw new BadRequestException('Invalid JSON provided: both attributes and relationship are empty.');
}
// Make sure that the attributes and relationships are accessible.
$element = empty($item['attributes']) ? array() : $item['attributes'];
$relationships = empty($item['relationships']) ? array() : $item['relationships'];
// For every relationship we need to see if it was included.
foreach ($relationships as $field_name => $relationship) {
if (empty($relationship['data'])) {
throw new BadRequestException('Invalid JSON provided: relationship without data.');
}
$data = $relationship['data'];
// It's always weird to deal with lists of items vs a single item.
$single_item = !ResourceFieldBase::isArrayNumeric($data);
// Make sure we're always dealing with a list of items.
$data = $single_item ? array($data) : $data;
$element[$field_name] = array();
foreach ($data as $info_pair) {
// Validate the JSON API structure for a relationship.
if (empty($info_pair['type'])) {
throw new BadRequestException('Invalid JSON provided: relationship item without type.');
}
if (empty($info_pair['id'])) {
throw new BadRequestException('Invalid JSON provided: relationship item without id.');
}
// Initialize the object if empty.
if (
!empty($info_pair['meta']['subrequest']) &&
$included_item = static::retrieveIncludedItem($info_pair['type'], $info_pair['id'], $included)
) {
// If the relationship was included, restructure it and embed it.
$value = array(
'body' => static::restructureItem($included_item, $included),
'id' => $info_pair['id'],
'request' => $info_pair['meta']['subrequest'],
);
if (!empty($value['request']['method']) && $value['request']['method'] == RequestInterface::METHOD_POST) {
// If the value is a POST remove the ID, since we already
// retrieved the included item.
unset($value['id']);
}
$element[$field_name][] = $value;
}
else {
// If the include could not be retrieved, use the ID instead.
$element[$field_name][] = array('id' => $info_pair['id']);
}
}
// Make the single relationships to be a single item or a single ID.
$element[$field_name] = $single_item ? reset($element[$field_name]) : $element[$field_name];
}
return $element;
}

/**
* Retrieves an item from the included pool of items.
*
* @param string $type
* The resource type.
* @param string $id
* The resource identifier.
* @param array $included
* All the available included elements.
*
* @return array
* The JSON API element.
*/
protected static function retrieveIncludedItem($type, $id, array $included) {
foreach ($included as $item) {
if (
!empty($item['type']) &&
$item['type'] == $type &&
!empty($item['id']) &&
$item['id'] == $id
) {
return $item;
}
}
return NULL;
}

}
5 changes: 1 addition & 4 deletions src/Plugin/resource/DataProvider/DataProviderEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ public function update($identifier, $object, $replace = FALSE) {
// The access calls use the request method. Fake the view to be a GET.
$old_request = $this->getRequest();
$this->getRequest()->setMethod(RequestInterface::METHOD_GET);
$output = array($this->view($wrapper->getIdentifier()));
$output = array($this->view($identifier));
// Put the original request back to a PUT/PATCH.
$this->request = $old_request;

Expand Down Expand Up @@ -823,9 +823,6 @@ protected function setPropertyValues(\EntityDrupalWrapper $wrapper, $object, $re
throw new BadRequestException('Bad input data provided. Please, check your input and your Content-Type header.');
}
$save = FALSE;
// We cannot set the 'id' property of the $object, it's only needed to know
// which entity to update. Remove it from the properties to set.
unset($object['id']);
$original_object = $object;
$interpreter = new DataInterpreterEMW($this->getAccount(), $wrapper);
// Keeps a list of the fields that have been set.
Expand Down
Loading

0 comments on commit a7b6175

Please sign in to comment.