diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 90264ca2..75fd68ba 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -285,11 +285,6 @@ parameters: count: 1 path: src/contracts/Input/Handler.php - - - message: "#^Method Ibexa\\\\Contracts\\\\Rest\\\\Input\\\\Parser\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/contracts/Input/Parser.php - - message: "#^Cannot access offset mixed on Ibexa\\\\Contracts\\\\Rest\\\\Input\\\\Parser\\.$#" count: 2 @@ -2165,31 +2160,16 @@ parameters: count: 1 path: src/lib/Server/Input/Parser/Aggregation/AbstractRangeAggregationParser.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\AbstractRangeAggregationParser\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Aggregation/AbstractRangeAggregationParser.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\AbstractRangeAggregationParser\\:\\:parseAggregation\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" count: 1 path: src/lib/Server/Input/Parser/Aggregation/AbstractRangeAggregationParser.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\AbstractStatsAggregationParser\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Aggregation/AbstractStatsAggregationParser.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\AbstractStatsAggregationParser\\:\\:parseAggregation\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" count: 1 path: src/lib/Server/Input/Parser/Aggregation/AbstractStatsAggregationParser.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\AbstractTermAggregationParser\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Aggregation/AbstractTermAggregationParser.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\AbstractTermAggregationParser\\:\\:parseAggregation\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" count: 1 @@ -2290,11 +2270,6 @@ parameters: count: 1 path: src/lib/Server/Input/Parser/Aggregation/ObjectStateTermAggregationParser.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\Range\\\\AbstractRangeParser\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Aggregation/Range/AbstractRangeParser.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Aggregation\\\\Range\\\\AbstractRangeParser\\:\\:visitRangeValue\\(\\) has no return type specified\\.$#" count: 1 @@ -2465,16 +2440,6 @@ parameters: count: 1 path: src/lib/Server/Input/Parser/Criterion/FullText.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\IsUserBased\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/IsUserBased.php - - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\IsUserEnabled\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/IsUserEnabled.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\LanguageCode\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" count: 1 @@ -2545,11 +2510,6 @@ parameters: count: 1 path: src/lib/Server/Input/Parser/Criterion/ObjectStateId.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\ObjectStateIdentifier\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/ObjectStateIdentifier.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\Operator\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" count: 1 @@ -2575,41 +2535,16 @@ parameters: count: 1 path: src/lib/Server/Input/Parser/Criterion/SectionId.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\SectionIdentifier\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/SectionIdentifier.php - - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\Sibling\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/Sibling.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\Subtree\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" count: 1 path: src/lib/Server/Input/Parser/Criterion/Subtree.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\UserEmail\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/UserEmail.php - - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\UserId\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/UserId.php - - message: "#^Parameter \\#1 \\$value of class Ibexa\\\\Contracts\\\\Core\\\\Repository\\\\Values\\\\Content\\\\Query\\\\Criterion\\\\UserId constructor expects array\\\\|int, array\\ given\\.$#" count: 1 path: src/lib/Server/Input/Parser/Criterion/UserId.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\UserLogin\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/Criterion/UserLogin.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Criterion\\\\UserMetadata\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" count: 1 @@ -2680,11 +2615,6 @@ parameters: count: 1 path: src/lib/Server/Input/Parser/FieldDefinitionUpdate.php - - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\JWTInput\\:\\:parse\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" - count: 1 - path: src/lib/Server/Input/Parser/JWTInput.php - - message: "#^Method Ibexa\\\\Rest\\\\Server\\\\Input\\\\Parser\\\\Limitation\\\\PathStringRouteBasedLimitationParser\\:\\:parseIdFromHref\\(\\) has parameter \\$limitationValue with no type specified\\.$#" count: 1 @@ -6675,16 +6605,6 @@ parameters: count: 2 path: tests/bundle/Functional/UserTest.php - - - message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Rest\\\\Functional\\\\ViewTest\\:\\:testViewRequestWithAndStatement\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/bundle/Functional/ViewTest.php - - - - message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Rest\\\\Functional\\\\ViewTest\\:\\:testViewRequestWithOrStatement\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/bundle/Functional/ViewTest.php - - message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Rest\\\\RequestParser\\\\RouterTest\\:\\:getRouterMock\\(\\) should return PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&Symfony\\\\Component\\\\Routing\\\\RouterInterface but returns Symfony\\\\Component\\\\Routing\\\\RouterInterface\\.$#" count: 1 diff --git a/src/bundle/Resources/config/input_parsers.yml b/src/bundle/Resources/config/input_parsers.yml index 379a707b..01ce90c8 100644 --- a/src/bundle/Resources/config/input_parsers.yml +++ b/src/bundle/Resources/config/input_parsers.yml @@ -291,6 +291,16 @@ services: tags: - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.internal.LocationQuery } + Ibexa\Rest\Server\Input\Parser\Criterion\Location\Depth: + parent: Ibexa\Rest\Server\Common\Parser + tags: + - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.internal.criterion.LocationDepth } + + Ibexa\Rest\Server\Input\Parser\Criterion\Location\IsMainLocation: + parent: Ibexa\Rest\Server\Common\Parser + tags: + - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.internal.criterion.IsMainLocation } + Ibexa\Rest\Server\Input\Parser\Criterion\Ancestor: parent: Ibexa\Rest\Server\Common\Parser class: Ibexa\Rest\Server\Input\Parser\Criterion\Ancestor diff --git a/src/contracts/Input/Parser.php b/src/contracts/Input/Parser.php index 657d6d8a..d640afe3 100644 --- a/src/contracts/Input/Parser.php +++ b/src/contracts/Input/Parser.php @@ -14,7 +14,7 @@ abstract class Parser /** * Parse input structure. * - * @param array $data + * @param array $data * @param \Ibexa\Contracts\Rest\Input\ParsingDispatcher $parsingDispatcher * * @return \Ibexa\Contracts\Core\Repository\Values\ValueObject|object diff --git a/src/lib/Server/Input/Parser/Criterion/Location/Depth.php b/src/lib/Server/Input/Parser/Criterion/Location/Depth.php new file mode 100644 index 00000000..7660b040 --- /dev/null +++ b/src/lib/Server/Input/Parser/Criterion/Location/Depth.php @@ -0,0 +1,84 @@ + Operator::IN, + 'EQ' => Operator::EQ, + 'GT' => Operator::GT, + 'GTE' => Operator::GTE, + 'LT' => Operator::LT, + 'LTE' => Operator::LTE, + 'BETWEEN' => Operator::BETWEEN, + ]; + + public function parse(array $data, ParsingDispatcher $parsingDispatcher): DepthCriterion + { + if (!array_key_exists('LocationDepth', $data)) { + throw new Exceptions\Parser('Invalid format'); + } + + $criterionData = $data['LocationDepth']; + if (!is_array($criterionData)) { + throw new Exceptions\Parser('Invalid format'); + } + + if (!isset($criterionData['Value'])) { + throw new Exceptions\Parser('Invalid format'); + } + + if ( + is_string($criterionData['Value']) + && is_numeric($criterionData['Value']) + && ((int)$criterionData['Value'] == $criterionData['Value']) + ) { + $criterionData['Value'] = (int)$criterionData['Value']; + } + + if (!in_array(gettype($criterionData['Value']), ['integer', 'array'], true)) { + throw new Exceptions\Parser('Invalid format'); + } + + $value = $criterionData['Value']; + + if (!isset($criterionData['Operator'])) { + throw new Exceptions\Parser('Invalid format'); + } + + $operator = $this->getOperator($criterionData['Operator']); + + return new DepthCriterion($operator, $value); + } + + /** + * Get operator for the given literal name. + */ + private function getOperator(string $operatorName): string + { + $operatorName = strtoupper($operatorName); + if (!isset(self::OPERATORS[$operatorName])) { + throw new Exceptions\Parser( + sprintf( + 'Unexpected LocationDepth operator. Expected one of: %s', + implode(', ', array_keys(self::OPERATORS)) + ) + ); + } + + return self::OPERATORS[$operatorName]; + } +} diff --git a/src/lib/Server/Input/Parser/Criterion/Location/IsMainLocation.php b/src/lib/Server/Input/Parser/Criterion/Location/IsMainLocation.php new file mode 100644 index 00000000..20093413 --- /dev/null +++ b/src/lib/Server/Input/Parser/Criterion/Location/IsMainLocation.php @@ -0,0 +1,26 @@ + format'); + } + + return new IsMainLocationCriterion($data['IsMainLocation']); + } +} diff --git a/tests/bundle/Functional/JsonSchema/View.json b/tests/bundle/Functional/JsonSchema/View.json new file mode 100644 index 00000000..9849516e --- /dev/null +++ b/tests/bundle/Functional/JsonSchema/View.json @@ -0,0 +1,136 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "View" + ], + "properties": { + "View": { + "type": "object", + "additionalProperties": false, + "required": [ + "_media-type", + "_href", + "identifier", + "Query", + "Result" + ], + "properties": { + "_media-type": { + "type": "string" + }, + "_href": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "Query": { + "type": "object", + "properties": { + "_media-type": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "_media-type" + ] + }, + "Result": { + "type": "object", + "additionalProperties": false, + "required": [ + "_media-type", + "_href", + "count", + "time", + "timedOut", + "maxScore", + "searchHits", + "aggregations" + ], + "properties": { + "_media-type": { + "type": "string" + }, + "_href": { + "type": "string" + }, + "count": { + "type": "number" + }, + "time": { + "type": "number" + }, + "timedOut": { + "type": "boolean" + }, + "maxScore": { + "oneOf": [ + { + "type": "number" + }, { + "type": "null" + } + ] + }, + "searchHits": { + "type": "object", + "additionalProperties": false, + "required": [ + "searchHit" + ], + "properties": { + "searchHit": { + "type": "array", + "items": { + "type": "object", + "required": [ + "_media-type", + "_score", + "_index", + "value" + ], + "additionalProperties": false, + "properties": { + "_media-type": { + "type": "string" + }, + "_score": { + "type": "number" + }, + "_index": { + "type": "string" + }, + "value": { + "type": "object", + "required": [ + "_media-type", + "Content" + ], + "additionalProperties": false, + "properties": { + "_media-type": { + "type": "string" + }, + "Content": { + "type": "object" + } + } + } + } + } + } + } + }, + "aggregations": { + "type": "array" + } + } + } + } + } + } +} diff --git a/tests/bundle/Functional/ViewTest.php b/tests/bundle/Functional/ViewTest.php index 0477dcac..85199e5d 100644 --- a/tests/bundle/Functional/ViewTest.php +++ b/tests/bundle/Functional/ViewTest.php @@ -8,10 +8,24 @@ class ViewTest extends TestCase { + use ResourceAssertionsTrait; + + private const VIEW_ENDPOINT_ACCEPT_TYPE = 'View+json'; + private const VIEW_ENDPOINT_URL = '/api/ibexa/v2/views'; + + private const FORMAT_XML = 'xml'; + private const FORMAT_JSON = 'json'; + + private const OPERATOR_EQUALITY = 'eq'; + private const OPERATOR_IN = 'in'; + + private const CRITERION_LOCATION_DEPTH = 'LocationDepth'; + private const CRITERION_IS_MAIN_LOCATION = 'IsMainLocation'; + /** * Covers POST /views. */ - public function testViewRequestWithOrStatement() + public function testViewRequestWithOrStatement(): void { $fooRemoteId = md5('View test content foo'); $barRemoteId = md5('View test content bar'); @@ -36,23 +50,102 @@ public function testViewRequestWithOrStatement() XML; $request = $this->createHttpRequest( 'POST', - '/api/ibexa/v2/views', + self::VIEW_ENDPOINT_URL, 'ViewInput+xml', - 'View+json', + self::VIEW_ENDPOINT_ACCEPT_TYPE, $body ); $response = $this->sendHttpRequest($request); + self::assertHttpResponseCodeEquals($response, 200); + self::assertJsonResponseIsValid($response->getBody()->getContents(), 'View'); $responseData = json_decode($response->getBody(), true); self::assertEquals(2, $responseData['View']['Result']['count']); } + /** + * @dataProvider provideForViewTest + */ + public function testCriterions(string $body, string $type): void + { + $request = $this->createHttpRequest( + 'POST', + self::VIEW_ENDPOINT_URL, + "ViewInput+$type", + self::VIEW_ENDPOINT_ACCEPT_TYPE, + $body + ); + $response = $this->sendHttpRequest($request); + self::assertHttpResponseCodeEquals($response, 200); + self::assertJsonResponseIsValid($response->getBody()->getContents(), 'View'); + $responseData = json_decode($response->getBody(), true); + self::assertGreaterThan(0, $responseData['View']['Result']['count']); + } + + /** + * @return iterable + */ + public function provideForViewTest(): iterable + { + $template = static fn (string $criterion, string $operator, string $format): string => sprintf( + 'Criterion: %s / Operator: %s / Format: %s', + $criterion, + strtoupper($operator), + strtoupper($format), + ); + + yield $template(self::CRITERION_LOCATION_DEPTH, self::OPERATOR_EQUALITY, self::FORMAT_XML) => [ + $this->loadFile(__DIR__ . '/_input/search/LocationDepth.eq.xml'), + self::FORMAT_XML, + ]; + + yield $template(self::CRITERION_LOCATION_DEPTH, self::OPERATOR_EQUALITY, self::FORMAT_JSON) => [ + $this->loadFile(__DIR__ . '/_input/search/LocationDepth.eq.json'), + self::FORMAT_JSON, + ]; + + yield $template(self::CRITERION_LOCATION_DEPTH, self::OPERATOR_IN, self::FORMAT_XML) => [ + $this->loadFile(__DIR__ . '/_input/search/LocationDepth.in.xml'), + self::FORMAT_XML, + ]; + + yield $template(self::CRITERION_LOCATION_DEPTH, self::OPERATOR_IN, self::FORMAT_JSON) => [ + $this->loadFile(__DIR__ . '/_input/search/LocationDepth.in.json'), + self::FORMAT_JSON, + ]; + + yield $template(self::CRITERION_IS_MAIN_LOCATION, self::OPERATOR_EQUALITY, self::FORMAT_XML) => [ + $this->loadFile(__DIR__ . '/_input/search/IsMainLocation.xml'), + self::FORMAT_XML, + ]; + + yield $template(self::CRITERION_IS_MAIN_LOCATION, self::OPERATOR_EQUALITY, self::FORMAT_JSON) => [ + $this->loadFile(__DIR__ . '/_input/search/IsMainLocation.json'), + self::FORMAT_JSON, + ]; + } + + private function loadFile(string $filepath): string + { + $data = file_get_contents($filepath); + + if ($data === false) { + throw new \RuntimeException(sprintf( + 'Unable to get contents for file: "%s". Ensure it exists and is readable.', + $filepath, + )); + } + + return $data; + } + + /** * Covers POST /views. * * @depends testViewRequestWithOrStatement */ - public function testViewRequestWithAndStatement() + public function testViewRequestWithAndStatement(): void { $fooRemoteId = md5('View test content foo'); $barRemoteId = md5('View test content bar'); @@ -78,16 +171,16 @@ public function testViewRequestWithAndStatement() XML; $request = $this->createHttpRequest( 'POST', - '/api/ibexa/v2/views', + self::VIEW_ENDPOINT_URL, 'ViewInput+xml', - 'View+json', + self::VIEW_ENDPOINT_ACCEPT_TYPE, $body ); $response = $this->sendHttpRequest($request); + self::assertHttpResponseCodeEquals($response, 200); + self::assertJsonResponseIsValid($response->getBody()->getContents(), 'View'); $responseData = json_decode($response->getBody(), true); self::assertEquals(1, $responseData['View']['Result']['count']); } } - -class_alias(ViewTest::class, 'EzSystems\EzPlatformRestBundle\Tests\Functional\ViewTest'); diff --git a/tests/bundle/Functional/_input/search/IsMainLocation.json b/tests/bundle/Functional/_input/search/IsMainLocation.json new file mode 100644 index 00000000..dc08dbd7 --- /dev/null +++ b/tests/bundle/Functional/_input/search/IsMainLocation.json @@ -0,0 +1,12 @@ +{ + "ViewInput": { + "identifier": "TitleView", + "Query": { + "LocationFilter": { + "IsMainLocation": 1 + }, + "limit": 10, + "offset": 0 + } + } +} diff --git a/tests/bundle/Functional/_input/search/IsMainLocation.xml b/tests/bundle/Functional/_input/search/IsMainLocation.xml new file mode 100644 index 00000000..bca5e8da --- /dev/null +++ b/tests/bundle/Functional/_input/search/IsMainLocation.xml @@ -0,0 +1,11 @@ + + + TitleView + + + 1 + + 10 + 0 + + diff --git a/tests/bundle/Functional/_input/search/LocationDepth.eq.json b/tests/bundle/Functional/_input/search/LocationDepth.eq.json new file mode 100644 index 00000000..2560ff9b --- /dev/null +++ b/tests/bundle/Functional/_input/search/LocationDepth.eq.json @@ -0,0 +1,15 @@ +{ + "ViewInput": { + "identifier": "TitleView", + "Query": { + "LocationFilter": { + "LocationDepth": { + "Value": [1], + "Operator": "eq" + } + }, + "limit": 10, + "offset": 0 + } + } +} diff --git a/tests/bundle/Functional/_input/search/LocationDepth.eq.xml b/tests/bundle/Functional/_input/search/LocationDepth.eq.xml new file mode 100644 index 00000000..33062dda --- /dev/null +++ b/tests/bundle/Functional/_input/search/LocationDepth.eq.xml @@ -0,0 +1,16 @@ + + + TitleView + + + + eq + + 1 + + + + 10 + 0 + + diff --git a/tests/bundle/Functional/_input/search/LocationDepth.in.json b/tests/bundle/Functional/_input/search/LocationDepth.in.json new file mode 100644 index 00000000..3dbccd38 --- /dev/null +++ b/tests/bundle/Functional/_input/search/LocationDepth.in.json @@ -0,0 +1,15 @@ +{ + "ViewInput": { + "identifier": "TitleView", + "Query": { + "LocationFilter": { + "LocationDepth": { + "Value": [1, 2], + "Operator": "in" + } + }, + "limit": 10, + "offset": 0 + } + } +} diff --git a/tests/bundle/Functional/_input/search/LocationDepth.in.xml b/tests/bundle/Functional/_input/search/LocationDepth.in.xml new file mode 100644 index 00000000..aa591025 --- /dev/null +++ b/tests/bundle/Functional/_input/search/LocationDepth.in.xml @@ -0,0 +1,17 @@ + + + TitleView + + + + in + + 1 + 2 + + + + 10 + 0 + +