Skip to content

Add JSON API basic support #1036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 50 commits into from
Closed
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
58bcfb4
feat: add jsonapi format
Oct 3, 2016
7b8bd74
add phpunit test
Oct 6, 2016
fc8d0a8
wip: response.json is valid JSON API.
Oct 23, 2016
b9da3aa
feat: add behat test
Oct 23, 2016
ddc38d1
WIP: Update tests and improve normalize/denormalize
hectorh30 Apr 7, 2017
1b04cda
Apply php-cs-fixer changes
hectorh30 Apr 7, 2017
5d9846a
Get tests back to green
hectorh30 Apr 8, 2017
3bae015
Test node specific version in travis
hectorh30 Apr 8, 2017
7e2582a
Add php-cs-fixer style changes
hectorh30 Apr 8, 2017
6b21d46
WIP: Enable PATCH method support
hectorh30 Apr 8, 2017
e1d467b
Get POST of a RelatedDummy with an inhertied set field working
hectorh30 Apr 8, 2017
1421971
Fix tests
hectorh30 Apr 8, 2017
27a133b
Add PATCH test to jsonapi.feature
hectorh30 Apr 8, 2017
e2eaa3b
Get unit tests back to green
hectorh30 Apr 8, 2017
b1c05d8
Get behat suite back to green
hectorh30 Apr 8, 2017
e3e0cee
Fix CS
hectorh30 Apr 8, 2017
f2cc39c
Add PATCH support to the swagger documentation normalizer
hectorh30 Apr 8, 2017
ca3f390
Fix CS
hectorh30 Apr 8, 2017
6051c5c
Merge branch 'master' into feature/add-jsonapi-update
hectorh30 Apr 8, 2017
d9e4f8f
Add jsonapi-validator to appveyor CI
hectorh30 Apr 8, 2017
42eeb19
Add basic error serialization support
hectorh30 Apr 10, 2017
cc12778
Add basic pagination support
hectorh30 Apr 11, 2017
43754f3
WIP: Add basic pagination support
hectorh30 Apr 13, 2017
0bb3b69
Fix tests and CS
hectorh30 Apr 13, 2017
88d7c3a
Add pagination and ordering functional tests and fix CS
hectorh30 Apr 13, 2017
9262a63
WIP: Add basic filtering support
hectorh30 Apr 13, 2017
f894146
Add condition to apply jsonapi event listeners only on jsonapi requests
hectorh30 Apr 14, 2017
df5d978
Fix services names
hectorh30 Apr 14, 2017
8200134
Fix date_filter.feature
hectorh30 Apr 14, 2017
3506b30
Fix normalizer to detect id even when it is not in normalization group
hectorh30 Apr 14, 2017
a755f72
Remove last pending tests from jsonapi.feature
hectorh30 Apr 14, 2017
2089a57
Fix and add test to relationship normalization in collections
hectorh30 Apr 14, 2017
25445f0
Add support for serializing collection relationship attributes
hectorh30 Apr 14, 2017
7e178cf
Merge remote-tracking branch 'api-platform/master' into feature/add-j…
hectorh30 Apr 15, 2017
2f530ef
Fix CS
hectorh30 Apr 15, 2017
042b1d0
Fix PATCH support on swagger HTTP API interface
hectorh30 Apr 15, 2017
c61fcee
Fix CS
hectorh30 Apr 15, 2017
545b191
Add PATCH to allow_update and restore allow_update test on JSON API i…
hectorh30 Apr 15, 2017
5a46e42
Allow object-level validation errors
hectorh30 Apr 15, 2017
db6da8c
Fix CS
hectorh30 Apr 16, 2017
ca5916c
Mark jsonapi serializer services as private
hectorh30 Apr 17, 2017
4900b15
Apply requested changes
hectorh30 Apr 19, 2017
01dbe8e
Remove comments
hectorh30 Apr 19, 2017
9ad63c1
Merge branch 'master' into feature/add-jsonapi-update
hectorh30 Apr 19, 2017
a4fd58c
Apply review changes
hectorh30 Apr 25, 2017
77d34e6
Merge branch 'master' into feature/add-jsonapi-update
hectorh30 Apr 25, 2017
59e0716
Fix syntax error
hectorh30 Apr 25, 2017
33b6eb0
Fix CS
hectorh30 Apr 25, 2017
858f4ac
Fix ErrorNormalizer and variable names
hectorh30 Apr 26, 2017
39955cb
Add support for POSTing and PATCHing many-to-many relationships
hectorh30 Apr 28, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ matrix:
before_install:
- phpenv config-rm xdebug.ini || echo "xdebug not available"
- echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
- rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install 6.10.2
- npm install -g swagger-cli
- npm install -g jsonapi-validator
- if [[ $coverage = 1 ]]; then mkdir -p build/logs build/cov; fi
- if [[ $coverage = 1 ]]; then wget https://phar.phpunit.de/phpcov.phar; fi
- if [[ $coverage = 1 ]]; then wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar; fi
Expand Down
5 changes: 5 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ build: false
platform: x86
clone_folder: c:\projects\api-platform\core

environment:
nodejs_version: "6"

cache:
- '%LOCALAPPDATA%\Composer\files'

init:
- SET PATH=c:\tools\php71;%PATH%

install:
- ps: Install-Product node $env:nodejs_version
- npm install -g jsonapi-validator
- ps: Set-Service wuauserv -StartupType Manual
- cinst -y php
- cd c:\tools\php71
Expand Down
1 change: 1 addition & 0 deletions behat.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ default:
- 'FeatureContext': { doctrine: '@doctrine' }
- 'HydraContext'
- 'SwaggerContext'
- 'JsonApiContext': { doctrine: '@doctrine' }
- 'Behat\MinkExtension\Context\MinkContext'
- 'Behatch\Context\RestContext'
- 'Behatch\Context\JsonContext'
Expand Down
190 changes: 190 additions & 0 deletions features/bootstrap/JsonApiContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyFriend;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behatch\Context\RestContext;
use Behatch\Json\Json;
use Behatch\Json\JsonInspector;
use Doctrine\Common\Persistence\ManagerRegistry;

final class JsonApiContext implements Context
{
private $restContext;

private $inspector;

private $doctrine;

/**
* @var \Doctrine\Common\Persistence\ObjectManager
*/
private $manager;

public function __construct(ManagerRegistry $doctrine)
{
$this->doctrine = $doctrine;
$this->manager = $doctrine->getManager();
}

/**
* Gives access to the Behatch context.
*
* @param BeforeScenarioScope $scope
*
* @BeforeScenario
*/
public function gatherContexts(BeforeScenarioScope $scope)
{
/** @var InitializedContextEnvironment $environment */
$environment = $scope->getEnvironment();

$this->restContext = $environment->getContext(RestContext::class);

$this->inspector = new JsonInspector('javascript');
}

/**
* @Then I save the response
*/
public function iSaveTheResponse()
{
$content = $this->getContent();

if (null === ($decoded = json_decode($content))) {
throw new \RuntimeException('JSON response seems to be invalid');
}

$fileName = __DIR__.'/response.json';

file_put_contents($fileName, $content);

return $fileName;
}

/**
* @Then I validate it with jsonapi-validator
*/
public function iValidateItWithJsonapiValidator()
{
$fileName = $this->iSaveTheResponse();

$validationResponse = exec(sprintf('cd %s && jsonapi-validator -f response.json', __DIR__));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I repost my comment from #785 (comment) here, I'm really not a fan of this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It should be done in Travis directly. No need to run the command from PHP.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dunglas, my suggestion was to use \Sanpi\Behatch\Json\JsonInspector::validate() and not to run any command in Travis or PHP! 😛

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I agree with @meyerbaptiste didn't had the time to do it but we should do that


$isValidJsonapi = 'response.json is valid JSON API.' === $validationResponse;

if (!$isValidJsonapi) {
unlink($fileName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we could unlink just one time

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good catch. Thanks.


throw new \RuntimeException('JSON response seems to be invalid JSON API');
}

unlink($fileName);
}

/**
* Checks that given JSON node is equal to an empty array.
*
* @Then the JSON node :node should be an empty array
*/
public function theJsonNodeShouldBeAnEmptyArray($node)
{
$actual = $this->getValueOfNode($node);

if (!is_array($actual) || !empty($actual)) {
throw new \Exception(
sprintf("The node value is '%s'", json_encode($actual))
);
}
}

/**
* Checks that given JSON node is a number.
*
* @Then the JSON node :node should be a number
*/
public function theJsonNodeShouldBeANumber($node)
{
$actual = $this->getValueOfNode($node);

if (!is_numeric($actual)) {
throw new \Exception(
sprintf('The node value is `%s`', json_encode($actual))
);
}
}

/**
* Checks that given JSON node is not an empty string.
*
* @Then the JSON node :node should not be an empty string
*/
public function theJsonNodeShouldNotBeAnEmptyString($node)
{
$actual = $this->getValueOfNode($node);

if ($actual === '') {
throw new \Exception(
sprintf('The node value is `%s`', json_encode($actual))
);
}
}

private function getValueOfNode($node)
{
$json = $this->getJson();

return $this->inspector->evaluate($json, $node);
}

private function getJson()
{
return new Json($this->getContent());
}

private function getContent()
{
return $this->restContext->getMink()->getSession()->getDriver()->getContent();
}

/**
* @Given there is a RelatedDummy
*/
public function thereIsARelatedDummy()
{
$relatedDummy = new RelatedDummy();

$relatedDummy->setName('RelatedDummy with friends');

$this->manager->persist($relatedDummy);

$this->manager->flush();
}

/**
* @Given there is a DummyFriend
*/
public function thereIsADummyFriend()
{
$friend = new DummyFriend();

$friend->setName('DummyFriend');

$this->manager->persist($friend);

$this->manager->flush();
}
}
8 changes: 7 additions & 1 deletion features/doctrine/date_filter.feature
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ Feature: Date filter on collections
},
"hydra:search": {
"@type": "hydra:IriTemplate",
"hydra:template": "\/dummies{?id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,order[id],order[name],order[relatedDummy.symfony],dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],dummyBoolean,dummyFloat,dummyPrice,description[exists],relatedDummy.name[exists],dummyBoolean[exists]}",
"hydra:template": "\/dummies{?id,id[],name,alias,description,relatedDummy.name,relatedDummy.name[],relatedDummies,relatedDummies[],dummy,relatedDummies.name,order[id],order[name],order[description],order[relatedDummy.symfony],dummyDate[before],dummyDate[after],relatedDummy.dummyDate[before],relatedDummy.dummyDate[after],dummyFloat[between],dummyFloat[gt],dummyFloat[gte],dummyFloat[lt],dummyFloat[lte],dummyPrice[between],dummyPrice[gt],dummyPrice[gte],dummyPrice[lt],dummyPrice[lte],dummyBoolean,dummyFloat,dummyPrice,description[exists],relatedDummy.name[exists],dummyBoolean[exists]}",
"hydra:variableRepresentation": "BasicRepresentation",
"hydra:mapping": [
{
Expand Down Expand Up @@ -487,6 +487,12 @@ Feature: Date filter on collections
"property": "name",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "order[description]",
"property": "description",
"required": false
},
{
"@type": "IriTemplateMapping",
"variable": "order[relatedDummy.symfony]",
Expand Down
3 changes: 2 additions & 1 deletion features/integration/nelmio_api_doc.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ Feature: NelmioApiDoc integration

Scenario: Create a user
When I send a "GET" request to "/nelmioapidoc"
Then the response status code should be 200
And the response status code should be 200
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

And I should see text matching "AbstractDummy"
And I should see text matching "Dummy"
And I should see text matching "User"
And I should see text matching "Retrieves the collection of Dummy resources."
And I should see text matching "Creates a Dummy resource."
And I should see text matching "Deletes the Dummy resource."
And I should see text matching "Updates the Dummy resource."
And I should see text matching "Replaces the Dummy resource."
79 changes: 79 additions & 0 deletions features/jsonapi/collections.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
Feature: JSON API collections support
In order to use the JSON API hypermedia format
As a client software developer
I need to be able to retrieve valid JSON API responses for collection attributes on entities.

@createSchema
@dropSchema
Scenario: Correctly serialize a collection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your scenario isn't correct. You have to divide it into several scenarios, with one for each operation. Each scenario consist of a list of steps: Givens, Whens, Thens. In your scenario here, there is only a list of Whens, no Thens, it's a little insane.
If you want to know more about this, I invite you to follow this link: http://docs.behat.org/en/latest/user_guide.html
You can also take inspiration from what has been done for Hydra: https://github.com/api-platform/core/tree/master/features/hydra

When I add "Accept" header equal to "application/vnd.api+json"
And I add "Content-Type" header equal to "application/vnd.api+json"
And I send a "POST" request to "/circular_references" with body:
"""
{
"data": {}
}
"""
And I validate it with jsonapi-validator
And I send a "PATCH" request to "/circular_references/1" with body:
"""
{
"data": {
"relationships": {
"parent": {
"data": {
"type": "CircularReference",
"id": "1"
}
}
}
}
}
"""
And I validate it with jsonapi-validator
And I send a "POST" request to "/circular_references" with body:
"""
{
"data": {
"relationships": {
"parent": {
"data": {
"type": "CircularReference",
"id": "1"
}
}
}
}
}
"""
And I validate it with jsonapi-validator
And I send a "GET" request to "/circular_references/1"
And the JSON should be equal to:
"""
{
"data": {
"id": "1",
"type": "CircularReference",
"relationships": {
"parent": {
"data": {
"type": "CircularReference",
"id": "1"
}
},
"children": {
"data": [
{
"type": "CircularReference",
"id": "1"
},
{
"type": "CircularReference",
"id": "2"
}
]
}
}
}
}
"""
Loading