Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d779714
IBX-8176: Implemented routing language expression extension handling …
barw4 Jun 5, 2024
4e82b59
IBX-8176: CS
barw4 Jun 5, 2024
d219aeb
IBX-8176: Added unit tests
barw4 Jun 5, 2024
f37be24
IBX-8176: Enable `strict_types`
barw4 Jun 5, 2024
4df7a28
IBX-8176: Enable `strict_types`
barw4 Jun 5, 2024
fd5bfbd
IBX-8176: Applied review remarks
barw4 Jun 5, 2024
aeeab61
IBX-8176: Fixed type
barw4 Jun 5, 2024
e49d954
IBX-8176: Refactored solution
barw4 Jun 18, 2024
1f2fd8d
IBX-8176: Refactoring
barw4 Jun 19, 2024
a1d680f
IBX-8176: Rename the expression function
barw4 Jun 19, 2024
2245b52
IBX-8176: Added generation of `options` route for newly added `condit…
barw4 Jun 27, 2024
de4652f
IBX-8176: Fixed tests
barw4 Jun 28, 2024
1968a96
IBX-8176: Created new OpenAPI compatible location moving endpoint
barw4 May 20, 2024
6df49ca
IBX-8176: Removed deprecation
barw4 May 27, 2024
9385eb8
IBX-8176: CS
barw4 Jul 2, 2024
14c6368
IBX-8176: Implemented routing language expression extension handling …
barw4 Jun 5, 2024
f06ee83
IBX-8176: Added unit tests
barw4 Jun 5, 2024
114ac97
IBX-8176: Applied review remarks
barw4 Jun 5, 2024
2f198fe
IBX-8176: Refactored solution
barw4 Jun 18, 2024
2363318
IBX-8176: Refactoring
barw4 Jun 19, 2024
2423844
IBX-8176: Rename the expression function
barw4 Jun 19, 2024
8a4f5af
IBX-8180: OpenAPI compatible location swap endpoint
barw4 Jun 7, 2024
35e56ab
IBX-8180: Added integration test
barw4 Jun 7, 2024
f785099
IBX-8180: Applied review remarks
barw4 Jun 20, 2024
54739e4
IBX-8180: Updated functional test
barw4 Jun 23, 2024
cb2b701
IBX-8180: Fixed routing definition
barw4 Jun 24, 2024
4178b4b
IBX-8180: Update route definition and functional test
barw4 Jun 27, 2024
fb7502a
IBX-8180: Update functional test
barw4 Jun 27, 2024
76c7a2e
IBX-8180: Update functional tests
barw4 Jun 28, 2024
0c19ebe
IBX-8180: Updated routing
barw4 Jun 28, 2024
b0fb202
IBX-8180: Applied review remarks
barw4 Jul 1, 2024
e9a49d5
IBX-8180: Fixed routing
barw4 Jul 2, 2024
989b2f1
IBX-8180: Applied review remark
barw4 Jul 3, 2024
7e42cc8
IBX-8180: Fixed routing after rebase
barw4 Jul 16, 2024
3343273
IBX-8180: Fixed routing after rebase
barw4 Jul 16, 2024
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"symfony/routing": "^5.3",
"symfony/security-bundle": "^5.3",
"symfony/security-csrf": "^5.3",
"symfony/yaml": "^5.3"
"symfony/yaml": "^5.3",
"webmozart/assert": "^1.11"
},
"require-dev": {
"ibexa/ci-scripts": "^0.2@dev",
Expand Down
8 changes: 8 additions & 0 deletions src/bundle/Resources/config/input_parsers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,11 @@ services:
$validator: '@validator'
tags:
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.MoveLocationInput }

Ibexa\Rest\Server\Input\Parser\SwapLocationInput:
parent: Ibexa\Rest\Server\Common\Parser
arguments:
$locationService: '@ibexa.api.service.location'
$validator: '@validator'
tags:
- { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.SwapLocationInput }
11 changes: 10 additions & 1 deletion src/bundle/Resources/config/routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -432,8 +432,17 @@ ibexa.rest.languages.view:

# Locations

ibexa.rest.location.swap:
path: /content/locations/{locationPath}
controller: Ibexa\Rest\Server\Controller\Location::swap
condition: 'ibexa_get_media_type(request) === "SwapLocationInput"'
methods: [POST]
options:
options_route_suffix: 'SwapLocationInput'
requirements:
locationPath: "[0-9/]+"

ibexa.rest.move_location:
ibexa.rest.location.move:
path: /content/locations/{locationPath}
controller: Ibexa\Rest\Server\Controller\Location::moveLocation
condition: 'ibexa_get_media_type(request) === "MoveLocationInput"'
Expand Down
21 changes: 21 additions & 0 deletions src/lib/Server/Controller/Location.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,27 @@ public function swapLocation($locationPath, Request $request)
return new Values\NoContent();
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
*/
public function swap(Request $request, string $locationPath): Values\NoContent
{
$locationId = $this->extractLocationIdFromPath($locationPath);
$location = $this->locationService->loadLocation($locationId);

$destinationLocation = $this->inputDispatcher->parse(
new Message(
['Content-Type' => $request->headers->get('Content-Type')],
$request->getContent(),
),
);

$this->locationService->swapLocation($location, $destinationLocation);

return new Values\NoContent();
}

/**
* Loads a location by remote ID.
*
Expand Down
91 changes: 91 additions & 0 deletions src/lib/Server/Input/Parser/AbstractDestinationLocationParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Rest\Server\Input\Parser;

use Ibexa\Contracts\Core\Repository\LocationService;
use Ibexa\Contracts\Core\Repository\Values\Content\Location;
use Ibexa\Contracts\Rest\Input\ParsingDispatcher;
use Ibexa\Rest\Input\BaseParser;
use Ibexa\Rest\Server\Exceptions\ValidationFailedException;
use Ibexa\Rest\Server\Validation\Builder\Input\Parser\BaseInputParserValidatorBuilder;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Webmozart\Assert\Assert;

abstract class AbstractDestinationLocationParser extends BaseParser
{
protected const string PARSER = '';
public const string DESTINATION_KEY = 'destination';

public function __construct(
private readonly LocationService $locationService,
protected readonly ValidatorInterface $validator,
) {
}

/**
* @phpstan-param array{
* 'destination': string,
* } $data
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
* @throws \Ibexa\Contracts\Rest\Exceptions\Parser
*/
public function parse(array $data, ParsingDispatcher $parsingDispatcher): Location
{
$this->validateInputData($data);

return $this->getLocationByPath($data[self::DESTINATION_KEY]);
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
private function getLocationByPath(string $path): Location
{
return $this->locationService->loadLocation(
$this->extractLocationIdFromPath($path)
);
}

private function extractLocationIdFromPath(string $path): int
{
$pathParts = explode('/', $path);
$lastPart = array_pop($pathParts);

Assert::integerish($lastPart);

return (int)$lastPart;
}

/**
* @phpstan-assert array{
* 'destination': string,
* } $data
*
* @param array<mixed> $data
*
* @throws \Ibexa\Rest\Server\Exceptions\ValidationFailedException
*/
private function validateInputData(array $data): void
{
$builder = $this->getValidatorBuilder();
$builder->validateInputArray($data);
$violations = $builder->build()->getViolations();
if ($violations->count() > 0) {
throw new ValidationFailedException(
static::PARSER,
$violations,
);
}
}

abstract protected function getValidatorBuilder(): BaseInputParserValidatorBuilder;
}
72 changes: 5 additions & 67 deletions src/lib/Server/Input/Parser/MoveLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,77 +8,15 @@

namespace Ibexa\Rest\Server\Input\Parser;

use Ibexa\Contracts\Core\Repository\LocationService;
use Ibexa\Contracts\Core\Repository\Values\Content\Location;
use Ibexa\Contracts\Rest\Input\ParsingDispatcher;
use Ibexa\Rest\Input\BaseParser;
use Ibexa\Rest\Server\Exceptions\ValidationFailedException;
use Ibexa\Rest\Server\Validation\Builder\Input\Parser\BaseInputParserValidatorBuilder;
use Ibexa\Rest\Server\Validation\Builder\Input\Parser\MoveLocationInputValidatorBuilder;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class MoveLocation extends BaseParser
final class MoveLocation extends AbstractDestinationLocationParser
{
public const string DESTINATION_KEY = 'destination';
protected const string PARSER = 'MoveLocation';

public function __construct(
private readonly LocationService $locationService,
private readonly ValidatorInterface $validator,
) {
}

/**
* @phpstan-param array{
* 'destination': string,
* } $data
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
* @throws \Ibexa\Contracts\Rest\Exceptions\Parser
*/
public function parse(array $data, ParsingDispatcher $parsingDispatcher): Location
{
$this->validateInputData($data);

return $this->getLocationByPath($data[self::DESTINATION_KEY]);
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
private function getLocationByPath(string $path): Location
{
return $this->locationService->loadLocation(
$this->extractLocationIdFromPath($path)
);
}

private function extractLocationIdFromPath(string $path): int
{
$pathParts = explode('/', $path);

return (int)array_pop($pathParts);
}

/**
* @phpstan-assert array{
* 'destination': string,
* } $data
*
* @param array<mixed> $data
*
* @throws \Ibexa\Rest\Server\Exceptions\ValidationFailedException
*/
private function validateInputData(array $data): void
protected function getValidatorBuilder(): BaseInputParserValidatorBuilder
{
$builder = new MoveLocationInputValidatorBuilder($this->validator);
$builder->validateInputArray($data);
$violations = $builder->build()->getViolations();
if ($violations->count() > 0) {
throw new ValidationFailedException(
'MoveLocation',
$violations,
);
}
return new MoveLocationInputValidatorBuilder($this->validator);
}
}
22 changes: 22 additions & 0 deletions src/lib/Server/Input/Parser/SwapLocationInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Rest\Server\Input\Parser;

use Ibexa\Rest\Server\Validation\Builder\Input\Parser\BaseInputParserValidatorBuilder;
use Ibexa\Rest\Server\Validation\Builder\Input\Parser\SwapLocationInputValidatorBuilder;

final class SwapLocationInput extends AbstractDestinationLocationParser
{
protected const string PARSER = 'SwapLocationInput';

protected function getValidatorBuilder(): BaseInputParserValidatorBuilder
{
return new SwapLocationInputValidatorBuilder($this->validator);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Rest\Server\Validation\Builder\Input\Parser;

use Ibexa\Rest\Server\Input\Parser\SwapLocationInput;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints as Assert;

final class SwapLocationInputValidatorBuilder extends BaseInputParserValidatorBuilder
{
protected function buildConstraint(): Constraint
{
return new Assert\Collection(
[
SwapLocationInput::DESTINATION_KEY => [
new Assert\NotBlank(),
new Assert\Type('string'),
new Assert\Regex('/^(\/\d+)+$/'),
],
],
);
}
}
1 change: 1 addition & 0 deletions tests/bundle/Functional/HttpOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public function providerForTestHttpOptions(): array
['/content/objects/1/objectstates', ['GET', 'PATCH']],
['/content/locations', ['GET']],
['/content/locations/1/2', ['POST'], 'MoveLocationInput+json'],
['/content/locations/1/2', ['POST'], 'SwapLocationInput+json'],
['/content/locations/1/2', ['GET', 'PATCH', 'DELETE', 'COPY', 'MOVE', 'SWAP']],
['/content/locations/1/2/children', ['GET']],
['/content/objects/1/locations', ['GET', 'POST']],
Expand Down
52 changes: 51 additions & 1 deletion tests/bundle/Functional/LocationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ private function createUrlAlias(string $locationHref, string $urlAlias): string
/**
* @depends testMoveSubtree
*/
public function testMoveLocation(string $locationHref): void
public function testMoveLocation(string $locationHref): string
{
$request = $this->createHttpRequest(
'POST',
Expand All @@ -282,5 +282,55 @@ public function testMoveLocation(string $locationHref): void

self::assertHttpResponseCodeEquals($response, 201);
self::assertHttpResponseHasHeader($response, 'Location');

return $locationHref;
}

/**
* @depends testMoveLocation
*/
public function testSwap(string $locationHref): void
{
$request = $this->createHttpRequest(
'COPY',
$locationHref,
'',
'',
'',
['Destination' => '/api/ibexa/v2/content/locations/1/43']
);
$response = $this->sendHttpRequest($request);
$newCopiedLocation = $response->getHeader('Location')[0];

$request = $this->createHttpRequest(
'COPY',
$locationHref,
'',
'',
'',
['Destination' => '/api/ibexa/v2/content/locations/1/43']
);
$response = $this->sendHttpRequest($request);
$secondCopiedLocation = $response->getHeader('Location')[0];

$request = $this->createHttpRequest(
'POST',
$newCopiedLocation,
'SwapLocationInput+json',
'',
json_encode([
'SwapLocationInput' => [
'destination' => str_replace(
'/api/ibexa/v2/content/locations',
'',
$secondCopiedLocation,
),
],
], JSON_THROW_ON_ERROR),
);

$response = $this->sendHttpRequest($request);

self::assertHttpResponseCodeEquals($response, 204);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function getCriteriaPayloads(): iterable
'is container' => [
'json',
$this->buildJsonCriterionQuery('"IsContainerCriterion": true'),
10,
12,
],
'is not container' => [
'json',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function getCriteriaPayloads(): iterable
'is not user based' => [
'json',
$this->buildJsonCriterionQuery('"IsUserBasedCriterion": false'),
10,
12,
],
];
}
Expand Down
Loading