Skip to content

[WIP]IBX-9631: Implement parser fetching content fields from expression #1500

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

Draft
wants to merge 1 commit into
base: 4.6
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions src/bundle/Resources/config/services/utils.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,13 @@ services:

Ibexa\AdminUi\Util\:
resource: "../../../../lib/Util"

Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParserInterface:
alias: Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParser

Ibexa\AdminUi\Util\ContentTypeFieldsExpressionParser: ~

Ibexa\AdminUi\Util\ContentTypeFieldsExtractorInterface:
alias: Ibexa\AdminUi\Util\ContentTypeFieldsExtractor

Ibexa\AdminUi\Util\ContentTypeFieldsExtractor: ~
74 changes: 74 additions & 0 deletions src/lib/Util/ContentTypeFieldsExpressionDoctrineLexer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?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\AdminUi\Util;

use Doctrine\Common\Lexer\AbstractLexer;

/**
* @extends AbstractLexer<ContentTypeFieldsExpressionDoctrineLexer::T_*, string>
*/
final class ContentTypeFieldsExpressionDoctrineLexer extends AbstractLexer
{
public const T_LBRACE = 1;
public const T_RBRACE = 2;
public const T_COMMA = 3;
public const T_SLASH = 4;
public const T_WILDCARD = 5;
public const T_IDENTIFIER = 6;

/**
* @return list<string>
*/
protected function getCatchablePatterns(): array
{
return [
'[a-zA-Z_][a-zA-Z0-9_-]*',
'\*',
'[\{\},\/]',
];
}

/**
* @return list<string>
*/
protected function getNonCatchablePatterns(): array
{
return [
'\s+',
];
}

/**
* @param string $value
*/
protected function getType(&$value): int
{
if ($value === '{') {
return self::T_LBRACE;
}

if ($value === '}') {
return self::T_RBRACE;
}

if ($value === ',') {
return self::T_COMMA;
}

if ($value === '/') {
return self::T_SLASH;
}

if ($value === '*') {
return self::T_WILDCARD;
}

return self::T_IDENTIFIER;
}
}
173 changes: 173 additions & 0 deletions src/lib/Util/ContentTypeFieldsExpressionParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?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\AdminUi\Util;

use RuntimeException;

final class ContentTypeFieldsExpressionParser implements ContentTypeFieldsExpressionParserInterface
{
private ContentTypeFieldsExpressionDoctrineLexer $lexer;

public function __construct()
{
$this->lexer = new ContentTypeFieldsExpressionDoctrineLexer();
}

public function parseExpression(string $expression): array
{
// Content type group can be omitted therefore we need to know how many parts are there
$slashCount = substr_count($expression, '/');

$this->lexer->setInput($expression);
$this->lexer->moveNext();

$groupTokens = null; // Content type groups are optional
$contentTypeTokens = null;
$fieldTokens = null;

while ($this->lexer->lookahead !== null) {
$this->lexer->moveNext();

if ($slashCount === 2) {
$groupTokens = $this->parseSection();
$this->expectSlash();
$contentTypeTokens = $this->parseSection();
$this->expectSlash();
$fieldTokens = $this->parseSection();
} elseif ($slashCount === 1) {
$groupTokens = null;
$contentTypeTokens = $this->parseSection();
$this->expectSlash();
$fieldTokens = $this->parseSection();
} else {
throw new RuntimeException('Invalid expression, expected one or two T_SLASH delimiters.');
}
}

$parsedTokens = [
$groupTokens,
$contentTypeTokens,
$fieldTokens,
];

if (array_filter($parsedTokens) === []) {
throw new RuntimeException('Choosing every possible content type field is not allowed.');
}

return $parsedTokens;
}

/**
* @return non-empty-list<string>|null
*/
private function parseSection(): ?array
{
$items = [];

if ($this->lexer->token === null) {
throw new RuntimeException('A token inside a section cannot be empty.');
}

// Multiple elements between braces
if ($this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_LBRACE)) {
$items[] = $this->getTokenFromInsideBracket();

while ($this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_COMMA)) {
$items[] = $this->getTokenFromInsideBracket();
}

if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_RBRACE)) {
throw new RuntimeException('Expected T_RBRACE to close the list.');
}

$this->lexer->moveNext();
} else {
// Otherwise, expect a single identifier or wildcard.
$token = $this->expectIdentifierOrWildcard();

if ($token === null) {
return null;
}

$items[] = $token;
}

return $items;
}

private function getTokenFromInsideBracket(): string
{
$this->lexer->moveNext();

$token = $this->expectIdentifierOrWildcard();
if ($token === null) {
throw new RuntimeException('Wildcards cannot be mixed with identifiers inside the expression.');
}

return $token;
}

/**
* @throws \RuntimeException
*/
private function expectSlash(): void
{
if ($this->lexer->token === null) {
throw new RuntimeException(
sprintf(
'Expected token of type "%s" but got "null"',
ContentTypeFieldsExpressionDoctrineLexer::T_SLASH,
),
);
}

if (!$this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_SLASH)) {
throw new RuntimeException(
sprintf(
'Expected token of type "%s" but got "%s"',
ContentTypeFieldsExpressionDoctrineLexer::T_SLASH,
$this->lexer->token->type,
),
);
}

$this->lexer->moveNext();
}

private function expectIdentifierOrWildcard(): ?string
{
if ($this->lexer->token === null) {
throw new RuntimeException(
sprintf(
'Expected token of type "%s" but got "null"',
ContentTypeFieldsExpressionDoctrineLexer::T_SLASH,
),
);
}

if (!in_array(
$this->lexer->token->type,
[
ContentTypeFieldsExpressionDoctrineLexer::T_IDENTIFIER,
ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD,
],
true,
)) {
throw new RuntimeException('Expected an identifier or wildcard.');
}

$value = $this->lexer->token->isA(ContentTypeFieldsExpressionDoctrineLexer::T_WILDCARD)
? null
: $this->lexer->token->value;

$this->lexer->moveNext();

return $value;
}
}
19 changes: 19 additions & 0 deletions src/lib/Util/ContentTypeFieldsExpressionParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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\AdminUi\Util;

interface ContentTypeFieldsExpressionParserInterface
{
/**
* @return array{non-empty-list<string>|null, non-empty-list<string>|null, non-empty-list<string>|null}
*
* @throws \RuntimeException
*/
public function parseExpression(string $expression): array;
}
Loading
Loading