Skip to content
Merged
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
36 changes: 36 additions & 0 deletions src/Service/Url/ValidatesTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\Service\Url;

use Membrane\OpenAPIReader\Exception\InvalidOpenAPI;
use Membrane\OpenAPIReader\ValueObject\Valid\Identifier;

/** @internal */
final class ValidatesTemplate
{
public function __invoke(Identifier $identifier, string $url): void
{
$characters = str_split($url);

$insideVariable = false;
foreach ($characters as $character) {
if ($character === '{') {
if ($insideVariable) {
throw InvalidOpenAPI::urlNestedVariable($identifier);
}
$insideVariable = true;
} elseif ($character === '}') {
if (!$insideVariable) {
throw InvalidOpenAPI::urlLiteralClosingBrace($identifier);
}
$insideVariable = false;
}
}

if ($insideVariable) {
throw InvalidOpenAPI::urlUnclosedVariable($identifier);
}
}
}
38 changes: 38 additions & 0 deletions src/ValueObject/Valid/Server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\ValueObject\Valid;

interface Server
{
/**
* @return string
* The regex matching the URL.
*/
public function getPattern(): string;

/**
* @return list<string>
* ordered list of variable names
* matching their order of appearance within the URL.
*/
public function getVariableNames(): array;

/**
* @phpstan-assert-if-true false $this->isTemplated()
* @return bool
* true if the URL does not contain variables,
* false otherwise.
*/
public function isConcrete(): bool;

/**
* @phpstan-assert-if-true false $this->isConcrete()
* @phpstan-assert-if-true =non-empty-list<string> $this->getVariableNames()
* @return bool
* true if the URL does contain variables,
* false otherwise.
*/
public function isTemplated(): bool;
}
68 changes: 24 additions & 44 deletions src/ValueObject/Valid/V30/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,23 @@
namespace Membrane\OpenAPIReader\ValueObject\Valid\V30;

use Membrane\OpenAPIReader\Exception\InvalidOpenAPI;
use Membrane\OpenAPIReader\Service\Url;
use Membrane\OpenAPIReader\ValueObject\Partial;
use Membrane\OpenAPIReader\ValueObject\Valid;
use Membrane\OpenAPIReader\ValueObject\Valid\Identifier;
use Membrane\OpenAPIReader\ValueObject\Valid\Validated;
use Membrane\OpenAPIReader\ValueObject\Valid\Warning;

final class Server extends Validated
final class Server extends Validated implements Valid\Server
{
/**
* REQUIRED
*/
/** REQUIRED */
public readonly string $url;

/**
* When the url has a variable named in {brackets}
* This array MUST contain the definition of the corresponding variable.
*
* The name of the variable is mapped to the Server Variable Object
* @var array<string,ServerVariable>
* A map between variable name and its value
*/
public readonly array $variables;

Expand All @@ -34,9 +33,10 @@ public function __construct(
throw InvalidOpenAPI::serverMissingUrl($parentIdentifier);
}

parent::__construct($parentIdentifier->append($server->url));
$identifier = $parentIdentifier->append($server->url);
parent::__construct($identifier);

$this->url = $this->validateUrl($parentIdentifier, $server->url);
$this->url = $this->validateUrl($identifier, $server->url);

$this->variables = $this->validateVariables(
$this->getIdentifier(),
Expand All @@ -45,53 +45,33 @@ public function __construct(
);
}

/**
* Returns the list of variable names in order of appearance within the URL.
* @return array<int, string>
*/
public function getPattern(): string
{
$regex = preg_replace('#{[^/]+}#', '([^/]+)', $this->url);
assert(is_string($regex));
return $regex;
}

public function getVariableNames(): array
{
preg_match_all('#{[^/]+}#', $this->url, $result);

return array_map(fn($v) => trim($v, '{}'), $result[0]);
}

/**
* Returns the regex of the URL
*/
public function getPattern(): string
public function isConcrete(): bool
{
$regex = preg_replace('#{[^/]+}#', '([^/]+)', $this->url);
assert(is_string($regex));
return $regex;
return empty($this->variables);
}

private function validateUrl(Identifier $identifier, string $url): string
public function isTemplated(): bool
{
$characters = str_split($url);

$insideVariable = false;
foreach ($characters as $character) {
if ($character === '{') {
if ($insideVariable) {
throw InvalidOpenAPI::urlNestedVariable(
$identifier->append($url)
);
}
$insideVariable = true;
} elseif ($character === '}') {
if (!$insideVariable) {
throw InvalidOpenAPI::urlLiteralClosingBrace(
$identifier->append($url)
);
}
$insideVariable = false;
}
}
return !empty($this->getVariableNames());
}

if ($insideVariable) {
throw InvalidOpenAPI::urlUnclosedVariable($identifier->append($url));
}
private function validateUrl(Identifier $identifier, string $url): string
{
(new Url\ValidatesTemplate())($identifier, $url);

if (str_ends_with($url, '/') && $url !== '/') {
$this->addWarning(
Expand All @@ -104,7 +84,7 @@ private function validateUrl(Identifier $identifier, string $url): string
}

/**
* @param array<int,string> $UrlVariableNames
* @param string[] $UrlVariableNames
* @param Partial\ServerVariable[] $variables
* @return array<string,ServerVariable>
*/
Expand Down
6 changes: 3 additions & 3 deletions src/ValueObject/Valid/V30/ServerVariable.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ final class ServerVariable extends Validated
/**
* If not null:
* - It SHOULD NOT be empty
* - The "default" value SHOULD be contained within this list
* @var string[]
* - It SHOULD contain the "default" value
* @var list<string>|null
*/
public readonly ?array $enum;
public readonly array|null $enum;

public function __construct(
Identifier $identifier,
Expand Down
130 changes: 130 additions & 0 deletions src/ValueObject/Valid/V31/Server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\ValueObject\Valid\V31;

use Membrane\OpenAPIReader\Exception\InvalidOpenAPI;
use Membrane\OpenAPIReader\Service\Url;
use Membrane\OpenAPIReader\ValueObject\Partial;
use Membrane\OpenAPIReader\ValueObject\Valid;
use Membrane\OpenAPIReader\ValueObject\Valid\Identifier;
use Membrane\OpenAPIReader\ValueObject\Valid\Validated;
use Membrane\OpenAPIReader\ValueObject\Valid\Warning;

final class Server extends Validated implements Valid\Server
{
/** REQUIRED */
public readonly string $url;

/**
* When the url has a variable named in {brackets}
* This array MUST contain the definition of the corresponding variable.
* @var array<string,ServerVariable>
* A map between variable name and its value
*/
public readonly array $variables;

public function __construct(
Identifier $parentIdentifier,
Partial\Server $server,
) {
if (!isset($server->url)) {
throw InvalidOpenAPI::serverMissingUrl($parentIdentifier);
}

$identifier = $parentIdentifier->append($server->url);
parent::__construct($identifier);

$this->url = $this->validateUrl($identifier, $server->url);

$this->variables = $this->validateVariables(
$this->getIdentifier(),
$this->getVariableNames(),
$server->variables,
);
}

public function getPattern(): string
{
$regex = preg_replace('#{[^/]+}#', '([^/]+)', $this->url);
assert(is_string($regex));
return $regex;
}

public function getVariableNames(): array
{
preg_match_all('#{[^/]+}#', $this->url, $result);

return array_map(fn($v) => trim($v, '{}'), $result[0]);
}

public function isConcrete(): bool
{
return empty($this->variables);
}

public function isTemplated(): bool
{
return !empty($this->getVariableNames());
}

private function validateUrl(Identifier $identifier, string $url): string
{
(new Url\ValidatesTemplate())($identifier, $url);

if (str_ends_with($url, '/') && $url !== '/') {
$this->addWarning(
'paths begin with a forward slash, so servers need not end in one',
Warning::REDUNDANT_FORWARD_SLASH,
);
}

return rtrim($url, '/');
}

/**
* @param string[] $UrlVariableNames
* @param Partial\ServerVariable[] $variables
* @return array<string,ServerVariable>
*/
private function validateVariables(
Identifier $identifier,
array $UrlVariableNames,
array $variables,
): array {
$result = [];
foreach ($variables as $variable) {
if (!isset($variable->name)) {
throw InvalidOpenAPI::serverVariableMissingName($identifier);
}

if (!in_array($variable->name, $UrlVariableNames)) {
$this->addWarning(
sprintf(
'"variables" defines "%s" which is not found in "url".',
$variable->name
),
Warning::REDUNDANT_VARIABLE
);

continue;
}

$result[$variable->name] = new ServerVariable(
$identifier->append($variable->name),
$variable
);
}

$undefined = array_diff($UrlVariableNames, array_keys($result));
if (!empty($undefined)) {
throw InvalidOpenAPI::serverHasUndefinedVariables(
$identifier,
...$undefined,
);
}

return $result;
}
}
56 changes: 56 additions & 0 deletions src/ValueObject/Valid/V31/ServerVariable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Membrane\OpenAPIReader\ValueObject\Valid\V31;

use Membrane\OpenAPIReader\Exception\InvalidOpenAPI;
use Membrane\OpenAPIReader\ValueObject\Partial;
use Membrane\OpenAPIReader\ValueObject\Valid\Identifier;
use Membrane\OpenAPIReader\ValueObject\Valid\Validated;
use Membrane\OpenAPIReader\ValueObject\Valid\Warning;

final class ServerVariable extends Validated
{
/** REQUIRED */
public readonly string $default;

/**
* If not null:
* - It SHOULD NOT be empty
* - It SHOULD contain the "default" value
* @var list<string>|null
*/
public readonly array|null $enum;

public function __construct(
Identifier $identifier,
Partial\ServerVariable $serverVariable,
) {
parent::__construct($identifier);

if (!isset($serverVariable->default)) {
throw InvalidOpenAPI::serverVariableMissingDefault($identifier);
}

$this->default = $serverVariable->default;

if (isset($serverVariable->enum)) {
if (empty($serverVariable->enum)) {
$this->addWarning(
'If "enum" is defined, it SHOULD NOT be empty',
Warning::EMPTY_ENUM,
);
}

if (!in_array($serverVariable->default, $serverVariable->enum)) {
$this->addWarning(
'If "enum" is defined, the "default" SHOULD exist within it.',
Warning::IMPOSSIBLE_DEFAULT
);
}
}

$this->enum = $serverVariable->enum;
}
}
Loading
Loading