Add spec enum service overlap validation

This commit is contained in:
Chirag Aggarwal
2026-04-22 14:49:35 +05:30
parent 0aab3e9a43
commit affd5876ab
2 changed files with 263 additions and 0 deletions
+141
View File
@@ -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);
}
+122
View File
@@ -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);
}
}