mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Add spec enum service overlap validation
This commit is contained in:
@@ -297,6 +297,146 @@ class Specs extends Action
|
||||
];
|
||||
}
|
||||
|
||||
protected function verifyParsedSpec(array $spec): void
|
||||
{
|
||||
$services = [];
|
||||
foreach ($spec['tags'] ?? [] as $tag) {
|
||||
if (!\is_array($tag)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$service = $tag['name'] ?? null;
|
||||
if (!\is_string($service) || $service === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$services[$this->normalizeSdkName($service)] = $service;
|
||||
}
|
||||
|
||||
if (empty($services)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enums = [];
|
||||
$this->collectSpecEnumNames($spec, $enums);
|
||||
|
||||
if (empty($enums)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$overlaps = [];
|
||||
foreach ($services as $normalized => $service) {
|
||||
if (!isset($enums[$normalized])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($enums[$normalized] as $enum) {
|
||||
$overlaps[] = "{$enum} (service '{$service}', enum '{$enum}')";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($overlaps)) {
|
||||
throw new \RuntimeException(
|
||||
'Spec service names must not overlap enum names. Overlaps: '
|
||||
. \implode(', ', \array_unique($overlaps))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function collectSpecEnumNames(array $node, array &$enums, ?string $fallbackName = null): void
|
||||
{
|
||||
if (isset($node['enum']) && \is_array($node['enum'])) {
|
||||
$enumName = $this->getExplicitSpecEnumName($node)
|
||||
?? $this->getFallbackSpecEnumName($node, $fallbackName);
|
||||
|
||||
if (!\is_null($enumName)) {
|
||||
$this->addSpecEnumName($enums, $enumName);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isset($node['items'])
|
||||
&& \is_array($node['items'])
|
||||
&& isset($node['items']['enum'])
|
||||
&& \is_array($node['items']['enum'])
|
||||
) {
|
||||
$enumName = $this->getExplicitSpecEnumName($node['items'])
|
||||
?? $this->getExplicitSpecEnumName($node)
|
||||
?? $this->getFallbackSpecEnumName($node, $fallbackName);
|
||||
|
||||
if (!\is_null($enumName)) {
|
||||
$this->addSpecEnumName($enums, $enumName);
|
||||
}
|
||||
}
|
||||
|
||||
$explicitEnumName = $this->getExplicitSpecEnumName($node);
|
||||
if (!\is_null($explicitEnumName)) {
|
||||
$this->addSpecEnumName($enums, $explicitEnumName);
|
||||
}
|
||||
|
||||
foreach ($node as $key => $value) {
|
||||
if (!\is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->collectSpecEnumNames(
|
||||
$value,
|
||||
$enums,
|
||||
$this->getChildSpecEnumFallbackName($node, $key, $value, $fallbackName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function addSpecEnumName(array &$enums, string $name): void
|
||||
{
|
||||
$enums[$this->normalizeSdkName($name)][] = $this->formatSdkName($name);
|
||||
}
|
||||
|
||||
private function getExplicitSpecEnumName(array $node): ?string
|
||||
{
|
||||
$enumName = $node['x-enum-name'] ?? null;
|
||||
|
||||
return \is_string($enumName) && $enumName !== '' ? $enumName : null;
|
||||
}
|
||||
|
||||
private function getFallbackSpecEnumName(array $node, ?string $fallbackName): ?string
|
||||
{
|
||||
$name = $node['name'] ?? $fallbackName;
|
||||
|
||||
return \is_string($name) && $name !== '' ? $name : null;
|
||||
}
|
||||
|
||||
private function getChildSpecEnumFallbackName(
|
||||
array $parent,
|
||||
int|string $key,
|
||||
array $child,
|
||||
?string $fallbackName
|
||||
): ?string {
|
||||
if (isset($child['name']) && \is_string($child['name']) && $child['name'] !== '') {
|
||||
return $child['name'];
|
||||
}
|
||||
|
||||
if ($key === 'schema' || $key === 'items') {
|
||||
return $this->getFallbackSpecEnumName($parent, $fallbackName);
|
||||
}
|
||||
|
||||
if (\is_string($key) && !\in_array($key, ['components', 'content', 'definitions', 'parameters', 'paths', 'properties', 'responses'], true)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
return $fallbackName;
|
||||
}
|
||||
|
||||
private function formatSdkName(string $name): string
|
||||
{
|
||||
return \str_replace(' ', '', \ucwords(\str_replace(['-', '_', '/'], ' ', $name)));
|
||||
}
|
||||
|
||||
private function normalizeSdkName(string $name): string
|
||||
{
|
||||
return \strtolower((string) \preg_replace('/[^a-z0-9]/i', '', $name));
|
||||
}
|
||||
|
||||
public function getSDKPlatformsForRouteSecurity(array $routeSecurity): array
|
||||
{
|
||||
$sdkPlatforms = [];
|
||||
@@ -483,6 +623,7 @@ class Specs extends Action
|
||||
|
||||
try {
|
||||
$parsedSpecs = $specs->parse();
|
||||
$this->verifyParsedSpec($parsedSpecs);
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new \RuntimeException("Spec generation failed for {$platform} ({$format}): " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Platform\Tasks;
|
||||
|
||||
use Appwrite\Platform\Tasks\Specs;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TestSpecs extends Specs
|
||||
{
|
||||
public function verify(array $spec): void
|
||||
{
|
||||
$this->verifyParsedSpec($spec);
|
||||
}
|
||||
}
|
||||
|
||||
class SpecsTest extends TestCase
|
||||
{
|
||||
private TestSpecs $specs;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->specs = new TestSpecs();
|
||||
}
|
||||
|
||||
public function testVerifyParsedSpecFailsOnServiceEnumNameOverlap(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Locale (service \'locale\', enum \'Locale\')');
|
||||
|
||||
$this->specs->verify([
|
||||
'tags' => [
|
||||
[
|
||||
'name' => 'locale',
|
||||
'description' => 'Locale APIs',
|
||||
],
|
||||
],
|
||||
'paths' => [
|
||||
'/account/sessions/oauth2/{provider}' => [
|
||||
'get' => [
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'provider',
|
||||
'schema' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['en'],
|
||||
'x-enum-name' => 'Locale',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testVerifyParsedSpecFailsOnDerivedServiceEnumNameOverlap(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Locale (service \'locale\', enum \'Locale\')');
|
||||
|
||||
$this->specs->verify([
|
||||
'tags' => [
|
||||
[
|
||||
'name' => 'locale',
|
||||
'description' => 'Locale APIs',
|
||||
],
|
||||
],
|
||||
'paths' => [
|
||||
'/projects/{projectId}/templates/email/{type}/{locale}' => [
|
||||
'patch' => [
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'payload',
|
||||
'in' => 'body',
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'locale' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['en'],
|
||||
'x-enum-name' => null,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testVerifyParsedSpecAllowsDistinctServiceAndEnumNames(): void
|
||||
{
|
||||
$this->specs->verify([
|
||||
'tags' => [
|
||||
[
|
||||
'name' => 'locale',
|
||||
'description' => 'Locale APIs',
|
||||
],
|
||||
],
|
||||
'paths' => [
|
||||
'/projects/{projectId}/templates/email/{type}/{locale}' => [
|
||||
'patch' => [
|
||||
'parameters' => [
|
||||
[
|
||||
'name' => 'locale',
|
||||
'schema' => [
|
||||
'type' => 'string',
|
||||
'enum' => ['en'],
|
||||
'x-enum-name' => 'EmailTemplateLocale',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user