Follow-up from #1816354: Add a REST module, starting with DELETE: The REST module should allow POST requests to create entities. The request Content-Type format will be hard-coded to JSON-LD for now, everything else will be rejected.

The URL for POST requests should not contain an ID, since the entity ID will be generated by drupal. Example URL for nodes: /entity/node

The response should contain a HTTP Location header (example: Location: http://example.com/entity/node/123) and a response code of 201 on success. The response body should be empty.

Depends on #1838596: Add deserialize for JSON-LD.

Work is done in the POST-create-1839346 branch in the REST sandbox.

Comments

klausi’s picture

Issue tags: +WSCCI

Tagging.

webchick’s picture

Component: base system » rest.module
Priority: Normal » Major

This seems pretty important to having web services in core.

klausi’s picture

Status: Active » Needs review
StatusFileSize
new10.45 KB
new28.3 KB

Here is the first patch, based on #1834288: RESTfully request an entity with JSON-LD serialized response.

Note that we currently have to create 2 routes per resource operation, because of a limitation in our current route dumper:

+        // @todo We need a second route here because the routing system dumps
+        // paths with default values just once. Example: /entity/node/{id} is
+        // dumped to entity/node, but not to entity/node/%. Is that a bug or a
+        // feature?

De-serialization is not ready yet, so this patch simply creates a dummy test entity on incoming POST requests for testing.

klausi’s picture

StatusFileSize
new3.61 KB
new28.52 KB

New patch: instead of 2 routes we do special case POST requests now, which have no ID in our entity creation scenario. Other resource plugins can of course overwrite the ResourceBase::routes() method altogether and provide any routes they want. This is just about providing sensible defaults that also match our entity use case.

klausi’s picture

Tagging for Crell.

Crell’s picture

Status: Needs review » Needs work
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
@@ -54,24 +54,41 @@ public function permissions() {
+        // Special case for POST and resource creation: Add a route that does
+        // not require an ID.

This is worded awkwardly. "Special case for resource creation via POST" would make more sense, I think.

+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
@@ -54,24 +54,41 @@ public function permissions() {
+            // @todo Once http://drupal.org/node/1793520 is committed we will have
+            // route object avaialble in the controller so 'plugin' property
+            // should be changed to '_plugin'.

Typo. "available".

That said, I don't see why naming this plugin vs. _plugin is in any way related to the routing issue... It's an internal value with magic meaning not intended for use in the signature of the controller callable. So it gets an _ prefix.

+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
@@ -38,23 +40,24 @@ public function routes() {
+      $result = db_query("SELECT * FROM {watchdog} WHERE wid = :wid", array(':wid' => $id))
+        ->fetchObject();

Pedantic nitpick. $result is usually used only when we are getting back an interatable result object. In this case, we're getting back a single record. So, this should be called $record.

+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
@@ -38,23 +40,24 @@ public function routes() {
+        // Serialization is done here, so we indicate with NULL that there is no
+        // subsequent serialization necessary.
+        $response = new ResourceResponse(NULL, 200, array('Content-Type' => 'application/json'));

I am not comfortable with this sort of flagging. "Pass NULL to mean this thing or pass something else to mean this other thing" is a code smell.

+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
@@ -18,21 +20,66 @@
+      // Create responses have an empty body.
+      return new ResourceResponse(NULL, 201, array('Location' => $url));

This reads oddly. I think you mean "201 Created responses have an empty body." (Otherwise it sounds like "Create" is a verb, not a noun.)

+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -38,12 +38,32 @@ public function handle($plugin, Request $request, $id = NULL) {
+        // @todo what do we pass here as "type" to the serializer?
+        // De-serialization does not work yet, see http://drupal.org/node/1838596
+        // $received = $serializer->deserialize($received, $plugin, 'drupal_jsonld');
+        // @todo Remove hard-coded test entity here.
+        $received = entity_create('entity_test', array('name' => 'test', 'user_id' => 1));

I think resolving this to not hard code entity_test is an RTBC blocker.

+++ b/core/modules/rest/lib/Drupal/rest/ResourceResponse.php
@@ -0,0 +1,48 @@
+  /**
+   * Returns response data that should be serialized.
+   *
+   * @return mixed
+   *   Response data that should be serialized.
+   */
+  public function getResponseData() {
+    return $this->responseData;
+  }

Doesn't the Response class have a getContent() method that works just as well? I'm confused why this is necessary.

+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -0,0 +1,83 @@
+  /**
+   * Tests several valid and invalid read requests on all entity types.
+   */
+  public function testRead() {

False. :-) Test method should test one thing each, only. If there's common setup that needs to be done for them, that's what the setUp() method is for.

klausi’s picture

Status: Needs work » Needs review
StatusFileSize
new4.49 KB
new28.79 KB

That said, I don't see why naming this plugin vs. _plugin is in any way related to the routing issue... It's an internal value with magic meaning not intended for use in the signature of the controller callable. So it gets an _ prefix.

Currently it is used on the method signature of the controller, so leaving it as is for this issue. We should fix that in a follow-up independently of this issue.

I am not comfortable with this sort of flagging. "Pass NULL to mean this thing or pass something else to mean this other thing" is a code smell.

The responseData property in ResourceResponse is used to indicate if serialization should be done (value/object present ==> serialize; NULL ==> do not serialize). You mean we should add an additional boolean property that is more explicit? $serialize = TRUE/FALSE that has to be passed to the constructor?

I think resolving this to not hard code entity_test is an RTBC blocker.

Agreed. So we should get #1838596: Add deserialize for JSON-LD in as soon as possible.

Doesn't the Response class have a getContent() method that works just as well? I'm confused why this is necessary.

Yes, we discussed simply using the Response class. But since the contract of that class implies that the content must either be a string or an object with a __toString() method we decided to use our own class. We have arbitrary data that does not have to be __toString() compatible. So we would heavily abuse Response if we first put data objects int the $content, then pulling them out, serializing them and pushing them back into $content.

Test method should test one thing each, only. If there's common setup that needs to be done for them, that's what the setUp() method is for.

If that were unit tests I would agree. But executing WebTestBase is expensive and installing Drupal again just to test inactive resources does not make sense. So I recommend to keep it this way.

klausi’s picture

StatusFileSize
new12.49 KB

Simple reroll, after #1834288: RESTfully request an entity with JSON-LD serialized response got committed. No other changes.

klausi’s picture

Issue summary: View changes

added link to sandbox

klausi’s picture

StatusFileSize
new8.16 KB
new63.24 KB

New patch that includes #1838596: Add deserialize for JSON-LD. Deserialization does not fully work yet, so this contains some ugly workaround hacks in jsonld module. The REST module side should be ready now.

klausi’s picture

StatusFileSize
new3.41 KB
new62.22 KB

Updated patch: removed the very cruel hacks I did to jsonld module, interdiff contains only changes against #1838596: Add deserialize for JSON-LD. It works with minor restrictions now!

Status: Needs review » Needs work

The last submitted patch, rest-post-1839346-9.patch, failed testing.

klausi’s picture

Status: Needs work » Needs review
StatusFileSize
new3.64 KB
new63.54 KB

Rerolled patch on top of #1838596: Add deserialize for JSON-LD. Added entity type verification before creating the entity, improved request handler documentation.

Status: Needs review » Needs work

The last submitted patch, rest-post-1839346-12.patch, failed testing.

klausi’s picture

Status: Needs work » Needs review
StatusFileSize
new42.44 KB

Rerolled patch on top of #1838596: Add deserialize for JSON-LD.

Status: Needs review » Needs work

The last submitted patch, rest-post-1839346-14.patch, failed testing.

klausi’s picture

Status: Needs work » Needs review
StatusFileSize
new675 bytes
new42.34 KB

Of course, we need to remove the currently obsolete jsonld_test module from the test.

Status: Needs review » Needs work
Issue tags: -WSCCI

The last submitted patch, rest-post-1839346-16.patch, failed testing.

klausi’s picture

Status: Needs work » Needs review
Issue tags: +WSCCI

#16: rest-post-1839346-16.patch queued for re-testing.

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

Looked at the small recent interdiffs and decided that this one is RTBC too. Hopefully we can reroll this quickly its dependency #1838596: Add deserialize for JSON-LD goes in.

moshe weitzman’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new16.32 KB

Reroll after #1838596: Add deserialize for JSON-LD. I had to fix one conflict in ResourceBase.php that happened (I think) when we renamed to _plugin (with underscore). Hope I got it right.

dries’s picture

-      $this->assertHeader('Content-Type', 'application/vnd.drupal.ld+json');
+      $this->assertHeader('content-type', 'application/vnd.drupal.ld+json');

This seems wrong, or at least weird.

effulgentsia’s picture

Status: Needs review » Needs work
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/Derivative/EntityDerivative.php
@@ -43,6 +43,7 @@ public function getDerivativeDefinitions(array $base_plugin_definition) {
+          'serialization_class' => $entity_info['class'],
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -34,19 +34,28 @@ public function handle(Request $request, $id = NULL) {
+      $class = $definition['serialization_class'];
+      $unserialized = $serializer->deserialize($received, $class, 'drupal_jsonld');

As I mentioned in #1838676-27: Support dynamic entity type management, I think it would be better to pass the entity type for the 2nd parameter to deserialize() rather than the entity class. According to linclark in #1838676-30: Support dynamic entity type management, this parameter is required to be a class name, but I disagree: in the Symfony update that went in in #1834594-24: Update dependencies (Symfony and Twig) follow up fixes for Composer, the parameter got renamed to $type (in the interface, but not in some of Symfony's implementations), and regardless of its name, it just gets passed to the denormalizer, so it's up to the denormalizer to decide what meaning to give it. But, I'm ok with this patch going in as-is, and me taking up this issue in #1852812: Use cache to get entity type/bundle metadata from JSON-LD @type URI where some discussion of it is already under way.

effulgentsia’s picture

oops. didn't mean to change status.

moshe weitzman’s picture

Status: Needs work » Reviewed & tested by the community

Regarding lowercasing in #21, RestTestbase::httpRequest is lowercasing the headers that the web server sends. It does so for a good reason I'll comment its quote here

// Store the header keys lower cased to be more robust. Headers are case
// insensitive according to RFC 2616.
$this->responseHeaders[strtolower($parts[0])] = isset($parts[1]) ? trim($parts[1]) : '';

So I think this code is robust and reasonable. I assure you that the web server is sending Content-type (mixed case) as is conventional.

Thanks effulgentsia for agreeing to handle #22 in a follow-up.

Tests have passed so back to RTBC.

klausi’s picture

Status: Reviewed & tested by the community » Needs work

This will need some more research and is not committable at this point. Please be patient, I try to get back to this as soon as I have more details.

klausi’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new987 bytes
new16.08 KB

We have considered the implications of the current patch in our weekly REST call and decided to move this forward as is. We can fix outstanding issues in followup issues as they arise.

Back to RTBC, I also removed a temporary hack from the creation test case, which is fortunately not needed anymore.

damien tournoud’s picture

  /**
   * Responds to entity POST requests.
   *
   * @param mixed $id
   *   Ignored.

Is it me, or this is *very* sloppy? When we are going to have other operations on a collection (for example, GET), are we *also* going to have an empty variable being passed around? Couldn't we make the resource handler properly handle those collection operations instead of hacking around like this?

Also, in its current form, would the deserializer allow to set the entity id and revision id?

klausi’s picture

StatusFileSize
new2.24 KB
new16.65 KB

Every resource plugin operation has the same method signature:

$response = $resource->{$method}($id, $unserialized, $request);

rest.module is aimed at single entity operations currently, retrieving collections is a task for Views display plugins. See #1819760: Add a REST export display plugin and serializer integration.. Since the POST operation creates an entity in the entity resource plugin semantics it does not need the ID parameter and can ignore it.

The deserializer ignores the ID completely right now. I added an isNew() check before saving the new entity. I'm not sure about revision IDs, I think we can ignore them for now and we should create followup issues for that.

Updated patch attached.

damien tournoud’s picture

@klausi: POST is a collection-level action, not a resource-level action. Those should not be handled the same way. It seems like the design needs to be fixed.

Crell’s picture

POST is not a collection-level action inherently. It's "instructions presented to a resource for processing", where that processing is undefined by HTTP. There's no reason you can't POST to a resource.

effulgentsia’s picture

Yes. But in the context of this issue, only a POST to create a new resource is implemented. I don't mind the extra unused $id in the signature, since it creates consistency with the other http methods in the module, though if a follow up wants to explore cleaning that up, that's cool too.

dries’s picture

Status: Reviewed & tested by the community » Fixed

It seems like Moshe, Crell and others are fine with the implementation (myself included), despite DamZ not being fully on board with the implementation details. I decided to go ahead and commit this. We can do clean up in a follow-up patch if enough people care.

Automatically closed -- issue fixed for 2 weeks with no activity.

Anonymous’s picture

Issue summary: View changes

added deserilaize dependency.