diff --git a/app/tasks/specs.php b/app/tasks/specs.php index 3db0a098c7..7bfcfe7750 100644 --- a/app/tasks/specs.php +++ b/app/tasks/specs.php @@ -123,105 +123,94 @@ $cli ], ]; - foreach (['swagger2', 'open-api3'] as $format) { - foreach ($platforms as $platform) { - $routes = []; - $models = []; - $services = []; + foreach ($platforms as $platform) { + $routes = []; + $models = []; + $services = []; - foreach ($appRoutes as $key => $method) { - foreach ($method as $route) { - /** @var \Utopia\Route $route */ - $routeSecurity = $route->getLabel('sdk.auth', []); - $sdkPlatofrms = []; + foreach ($appRoutes as $key => $method) { + foreach ($method as $route) { + /** @var \Utopia\Route $route */ + $routeSecurity = $route->getLabel('sdk.auth', []); + $sdkPlatofrms = []; - foreach ($routeSecurity as $value) { - switch ($value) { - case APP_AUTH_TYPE_SESSION: - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; - break; - case APP_AUTH_TYPE_KEY: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; - break; - case APP_AUTH_TYPE_JWT: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; - break; - case APP_AUTH_TYPE_ADMIN: - $sdkPlatofrms[] = APP_PLATFORM_CONSOLE; - break; - } + foreach ($routeSecurity as $value) { + switch ($value) { + case APP_AUTH_TYPE_SESSION: + $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + break; + case APP_AUTH_TYPE_KEY: + $sdkPlatofrms[] = APP_PLATFORM_SERVER; + break; + case APP_AUTH_TYPE_JWT: + $sdkPlatofrms[] = APP_PLATFORM_SERVER; + break; + case APP_AUTH_TYPE_ADMIN: + $sdkPlatofrms[] = APP_PLATFORM_CONSOLE; + break; } - - if (empty($routeSecurity)) { - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; - } - - if (!$route->getLabel('docs', true)) { - continue; - } - - if ($route->getLabel('sdk.mock', false) && !$mocks) { - continue; - } - - if (!$route->getLabel('sdk.mock', false) && $mocks) { - continue; - } - - if (empty($route->getLabel('sdk.namespace', null))) { - continue; - } - - if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $sdkPlatofrms)) { - continue; - } - - $routes[] = $route; - $modelLabel = $route->getLabel('sdk.response.model', 'none'); - \is_array($modelLabel) ? \array_map(function ($m) use ($response) { - return $response->getModel($m); - }, $modelLabel) : $response->getModel($modelLabel); } - } - foreach (Config::getParam('services', []) as $service) { - if ( - !isset($service['docs']) // Skip service if not part of the public API - || !isset($service['sdk']) - || !$service['docs'] - || !$service['sdk'] - ) { + if (empty($routeSecurity)) { + $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + } + + if (!$route->getLabel('docs', true)) { continue; } - $services[] = [ - 'name' => $service['key'] ?? '', - 'description' => $service['subtitle'] ?? '', - 'x-globalAttributes' => $service['globalAttributes'] ?? [], - ]; - } - - $models = $response->getModels(); - - foreach ($models as $key => $value) { - if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) { - unset($models[$key]); + if ($route->getLabel('sdk.mock', false) && !$mocks) { + continue; } + + if (!$route->getLabel('sdk.mock', false) && $mocks) { + continue; + } + + if (empty($route->getLabel('sdk.namespace', null))) { + continue; + } + + if ($platform !== APP_PLATFORM_CONSOLE && !\in_array($platforms[$platform], $sdkPlatofrms)) { + continue; + } + + $routes[] = $route; + } + } + + foreach (Config::getParam('services', []) as $service) { + if ( + !isset($service['docs']) // Skip service if not part of the public API + || !isset($service['sdk']) + || !$service['docs'] + || !$service['sdk'] + ) { + continue; } - switch ($format) { - case 'swagger2': - $formatInstance = new Swagger2(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0); - break; + $services[] = [ + 'name' => $service['key'] ?? '', + 'description' => $service['subtitle'] ?? '', + 'x-globalAttributes' => $service['globalAttributes'] ?? [], + ]; + } - case 'open-api3': - $formatInstance = new OpenAPI3(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0); - break; + $models = $response->getModels(); - default: - throw new Exception('Format not found: ' . $format); - break; + foreach ($models as $key => $value) { + if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) { + unset($models[$key]); } + } + // var_dump($models); + $arguments = [new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0]; + foreach (['swagger2', 'open-api3'] as $format) { + $formatInstance = match($format) { + 'swagger2' => new Swagger2(...$arguments), + 'open-api3' => new OpenAPI3(...$arguments), + default => throw new Exception('Format not found: ' . $format) + }; $specs = new Specification($formatInstance); $endpoint = App::getEnv('_APP_HOME', '[HOSTNAME]'); diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php index 44696700f0..91120e74c6 100644 --- a/src/Appwrite/Specification/Format.php +++ b/src/Appwrite/Specification/Format.php @@ -8,40 +8,22 @@ use Appwrite\Utopia\Response\Model; abstract class Format { - /** - * @var App - */ - protected $app; - - /** - * @var array - */ - protected $services; + protected App $app; /** * @var Route[] */ - protected $routes; + protected array $routes; /** * @var Model[] */ - protected $models; + protected array $models; - /** - * @var array - */ - protected $keys; - - /** - * @var int - */ - protected $authCount; - - /** - * @var array - */ - protected $params = [ + protected array $services; + protected array $keys; + protected int $authCount; + protected array $params = [ 'name' => '', 'description' => '', 'endpoint' => 'https://localhost', @@ -56,14 +38,6 @@ abstract class Format 'license.url' => '', ]; - /** - * @param App $app - * @param array $services - * @param Route[] $routes - * @param Model[] $models - * @param array $keys - * @param int $authCount - */ public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount) { $this->app = $app; @@ -121,10 +95,6 @@ abstract class Format */ public function getParam(string $key, string $default = ''): string { - if (!isset($this->params[$key])) { - return $default; - } - - return $this->params[$key]; + return $this->params[$key] ?? $default; } } diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index f36b532366..0045233813 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -4,35 +4,40 @@ namespace Appwrite\Specification\Format; use Appwrite\Specification\Format; use Appwrite\Template\Template; +use Appwrite\Utopia\Response\Model; use Utopia\Validator; class OpenAPI3 extends Format { - /** - * Get Name. - * - * Get format name - * - * @return string - */ public function getName(): string { return 'Open API 3'; } - /** - * Parse - * - * Parses Appwrite App to given format - * - * @return array - */ + protected function getNestedModels(Model $model, array &$usedModels): void + { + foreach ($model->getRules() as $rule) { + if ( + in_array($model->getType(), $usedModels) + && !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double']) + ) { + $usedModels[] = $rule['type']; + foreach ($this->models as $m) { + if ($m->getType() === $rule['type']) { + $this->getNestedModels($m, $usedModels); + return; + } + } + } + } + } + public function parse(): array { /** - * Specifications (v3.0.0): - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md - */ + * Specifications (v3.0.0): + * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md + */ $output = [ 'openapi' => '3.0.0', 'info' => [ @@ -89,7 +94,7 @@ class OpenAPI3 extends Format $usedModels = []; - foreach ($this->routes as $route) { /** @var \Utopia\Route $route */ + foreach ($this->routes as $route) { $url = \str_replace('/v1', '', $route->getPath()); $scope = $route->getLabel('scope', ''); $hide = $route->getLabel('sdk.hide', false); @@ -104,34 +109,32 @@ class OpenAPI3 extends Format $produces = $route->getLabel('sdk.response.type', null); $model = $route->getLabel('sdk.response.model', 'none'); $routeSecurity = $route->getLabel('sdk.auth', []); - $sdkPlatofrms = []; + $sdkPlatforms = []; foreach ($routeSecurity as $value) { switch ($value) { case APP_AUTH_TYPE_SESSION: - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + $sdkPlatforms[] = APP_PLATFORM_CLIENT; break; case APP_AUTH_TYPE_KEY: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; + $sdkPlatforms[] = APP_PLATFORM_SERVER; break; case APP_AUTH_TYPE_JWT: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; + $sdkPlatforms[] = APP_PLATFORM_SERVER; break; case APP_AUTH_TYPE_ADMIN: - $sdkPlatofrms[] = APP_PLATFORM_CONSOLE; + $sdkPlatforms[] = APP_PLATFORM_CONSOLE; break; } } if (empty($routeSecurity)) { - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + $sdkPlatforms[] = APP_PLATFORM_CLIENT; } $temp = [ 'summary' => $route->getDesc(), 'operationId' => $route->getLabel('sdk.namespace', 'default') . ucfirst($id), - // 'consumes' => [], - // 'produces' => [$produces], 'tags' => [$route->getLabel('sdk.namespace', 'default')], 'description' => ($desc) ? \file_get_contents($desc) : '', 'responses' => [], @@ -146,20 +149,14 @@ class OpenAPI3 extends Format 'rate-time' => $route->getLabel('abuse-time', 3600), 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), 'scope' => $route->getLabel('scope', ''), - 'platforms' => $sdkPlatofrms, + 'platforms' => $sdkPlatforms, 'packaging' => $route->getLabel('sdk.packaging', false), ], ]; - foreach ($this->models as $key => $value) { + foreach ($this->models as $value) { if (\is_array($model)) { - $model = \array_map(function ($m) use ($value) { - if ($m === $value->getType()) { - return $value; - } - - return $m; - }, $model); + $model = \array_map(fn ($m) => $m === $value->getType() ? $value : $m, $model); } else { if ($value->getType() === $model) { $model = $value; @@ -168,9 +165,9 @@ class OpenAPI3 extends Format } } - if (!(\is_array($model)) && $model->isNone()) { + if (!(\is_array($model)) && $model->isNone()) { $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [ - 'description' => (in_array($produces, [ + 'description' => in_array($produces, [ 'image/*', 'image/jpeg', 'image/gif', @@ -179,16 +176,11 @@ class OpenAPI3 extends Format 'image/svg-x', 'image/x-icon', 'image/bmp', - ])) ? 'Image' : 'File', - // 'schema' => [ - // 'type' => 'file' - // ], + ]) ? 'Image' : 'File', ]; } else { if (\is_array($model)) { - $modelDescription = \join(', or ', \array_map(function ($m) { - return $m->getName(); - }, $model)); + $modelDescription = \join(', or ', \array_map(fn ($m) => $m->getName(), $model)); // model has multiple possible responses, we will use oneOf foreach ($model as $m) { @@ -200,9 +192,7 @@ class OpenAPI3 extends Format 'content' => [ $produces => [ 'schema' => [ - 'oneOf' => \array_map(function ($m) { - return ['$ref' => '#/components/schemas/' . $m->getType()]; - }, $model) + 'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model) ], ], ], @@ -255,7 +245,10 @@ class OpenAPI3 extends Format $bodyRequired = []; foreach ($route->getParams() as $name => $param) { // Set params - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; /* @var $validator \Utopia\Validator */ + /** + * @var \Utopia\Validator $validator + */ + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; $node = [ 'name' => $name, @@ -329,7 +322,8 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'password'; $node['schema']['x-example'] = 'password'; break; - case 'Utopia\Validator\Range': /** @var \Utopia\Validator\Range $validator */ + case 'Utopia\Validator\Range': + /** @var \Utopia\Validator\Range $validator */ $node['schema']['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['schema']['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['schema']['x-example'] = $validator->getMin(); @@ -351,7 +345,8 @@ class OpenAPI3 extends Format $node['schema']['format'] = 'url'; $node['schema']['x-example'] = 'https://example.com'; break; - case 'Utopia\Validator\WhiteList': /** @var \Utopia\Validator\WhiteList $validator */ + case 'Utopia\Validator\WhiteList': + /** @var \Utopia\Validator\WhiteList $validator */ $node['schema']['type'] = $validator->getType(); $node['schema']['x-example'] = $validator->getList()[0]; @@ -413,20 +408,11 @@ class OpenAPI3 extends Format $temp['requestBody'] = $body; } - //$temp['consumes'] = $consumes; - $output['paths'][$url][\strtolower($route->getMethod())] = $temp; } foreach ($this->models as $model) { - foreach ($model->getRules() as $rule) { - if ( - in_array($model->getType(), $usedModels) - && !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float']) - ) { - $usedModels[] = $rule['type']; - } - } + $this->getNestedModels($model, $usedModels); } foreach ($this->models as $model) { diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index e1ddcf6d6f..aac805fd3d 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -4,34 +4,39 @@ namespace Appwrite\Specification\Format; use Appwrite\Specification\Format; use Appwrite\Template\Template; +use Appwrite\Utopia\Response\Model; use Utopia\Validator; class Swagger2 extends Format { - /** - * Get Name. - * - * Get format name - * - * @return string - */ public function getName(): string { return 'Swagger 2'; } - /** - * Parse - * - * Parses Appwrite App to given format - * - * @return array - */ + protected function getNestedModels(Model $model, array &$usedModels): void + { + foreach ($model->getRules() as $rule) { + if ( + in_array($model->getType(), $usedModels) + && !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double']) + ) { + $usedModels[] = $rule['type']; + foreach ($this->models as $m) { + if ($m->getType() === $rule['type']) { + $this->getNestedModels($m, $usedModels); + return; + } + } + } + } + } + public function parse(): array { /* - * Specifications (v3.0.0): - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md + * Specifications (v2.0): + * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md */ $output = [ 'swagger' => '2.0', @@ -87,7 +92,8 @@ class Swagger2 extends Format $usedModels = []; - foreach ($this->routes as $route) { /** @var \Utopia\Route $route */ + foreach ($this->routes as $route) { + /** @var \Utopia\Route $route */ $url = \str_replace('/v1', '', $route->getPath()); $scope = $route->getLabel('scope', ''); $hide = $route->getLabel('sdk.hide', false); @@ -102,27 +108,27 @@ class Swagger2 extends Format $produces = $route->getLabel('sdk.response.type', null); $model = $route->getLabel('sdk.response.model', 'none'); $routeSecurity = $route->getLabel('sdk.auth', []); - $sdkPlatofrms = []; + $sdkPlatforms = []; foreach ($routeSecurity as $value) { switch ($value) { case APP_AUTH_TYPE_SESSION: - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + $sdkPlatforms[] = APP_PLATFORM_CLIENT; break; case APP_AUTH_TYPE_KEY: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; + $sdkPlatforms[] = APP_PLATFORM_SERVER; break; case APP_AUTH_TYPE_JWT: - $sdkPlatofrms[] = APP_PLATFORM_SERVER; + $sdkPlatforms[] = APP_PLATFORM_SERVER; break; case APP_AUTH_TYPE_ADMIN: - $sdkPlatofrms[] = APP_PLATFORM_CONSOLE; + $sdkPlatforms[] = APP_PLATFORM_CONSOLE; break; } } if (empty($routeSecurity)) { - $sdkPlatofrms[] = APP_PLATFORM_CLIENT; + $sdkPlatforms[] = APP_PLATFORM_CLIENT; } $temp = [ @@ -144,7 +150,7 @@ class Swagger2 extends Format 'rate-time' => $route->getLabel('abuse-time', 3600), 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), 'scope' => $route->getLabel('scope', ''), - 'platforms' => $sdkPlatofrms, + 'platforms' => $sdkPlatforms, 'packaging' => $route->getLabel('sdk.packaging', false), ], ]; @@ -153,14 +159,9 @@ class Swagger2 extends Format $temp['produces'][] = $produces; } - foreach ($this->models as $key => $value) { + foreach ($this->models as $value) { if (\is_array($model)) { - $model = \array_map(function ($m) use ($value) { - if ($m === $value->getType()) { - return $value; - } - return $m; - }, $model); + $model = \array_map(fn ($m) => $m === $value->getType() ? $value : $m, $model); } else { if ($value->getType() === $model) { $model = $value; @@ -171,7 +172,7 @@ class Swagger2 extends Format if (!(\is_array($model)) && $model->isNone()) { $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [ - 'description' => (in_array($produces, [ + 'description' => in_array($produces, [ 'image/*', 'image/jpeg', 'image/gif', @@ -180,16 +181,14 @@ class Swagger2 extends Format 'image/svg-x', 'image/x-icon', 'image/bmp', - ])) ? 'Image' : 'File', + ]) ? 'Image' : 'File', 'schema' => [ 'type' => 'file' ], ]; } else { if (\is_array($model)) { - $modelDescription = \join(', or ', \array_map(function ($m) { - return $m->getName(); - }, $model)); + $modelDescription = \join(', or ', \array_map(fn ($m) => $m->getName(), $model)); // model has multiple possible responses, we will use oneOf foreach ($model as $m) { $usedModels[] = $m->getType(); @@ -244,7 +243,8 @@ class Swagger2 extends Format $bodyRequired = []; foreach ($route->getParams() as $name => $param) { // Set params - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; /** @var \Utopia\Validator $validator */ + /** @var \Utopia\Validator $validator */ + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['injections'])) : $param['validator']; $node = [ 'name' => $name, @@ -294,7 +294,6 @@ class Swagger2 extends Format $node['type'] = 'object'; $node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default']; $node['x-example'] = '{}'; - //$node['format'] = 'json'; break; case 'Utopia\Storage\Validator\File': $consumes = ['multipart/form-data']; @@ -320,7 +319,8 @@ class Swagger2 extends Format $node['format'] = 'password'; $node['x-example'] = 'password'; break; - case 'Utopia\Validator\Range': /** @var \Utopia\Validator\Range $validator */ + case 'Utopia\Validator\Range': + /** @var \Utopia\Validator\Range $validator */ $node['type'] = $validator->getType() === Validator::TYPE_FLOAT ? 'number' : $validator->getType(); $node['format'] = $validator->getType() == Validator::TYPE_INTEGER ? 'int32' : 'float'; $node['x-example'] = $validator->getMin(); @@ -342,7 +342,8 @@ class Swagger2 extends Format $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; break; - case 'Utopia\Validator\WhiteList': /** @var \Utopia\Validator\WhiteList $validator */ + case 'Utopia\Validator\WhiteList': + /** @var \Utopia\Validator\WhiteList $validator */ $node['type'] = $validator->getType(); $node['x-example'] = $validator->getList()[0]; @@ -410,14 +411,7 @@ class Swagger2 extends Format } foreach ($this->models as $model) { - foreach ($model->getRules() as $rule) { - if ( - in_array($model->getType(), $usedModels) - && !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float']) - ) { - $usedModels[] = $rule['type']; - } - } + $this->getNestedModels($model, $usedModels); } foreach ($this->models as $model) { @@ -485,15 +479,11 @@ class Swagger2 extends Format if (\is_array($rule['type'])) { if ($rule['array']) { $items = [ - 'x-anyOf' => \array_map(function ($type) { - return ['$ref' => '#/definitions/' . $type]; - }, $rule['type']) + 'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']) ]; } else { $items = [ - 'x-oneOf' => \array_map(function ($type) { - return ['$ref' => '#/definitions/' . $type]; - }, $rule['type']) + 'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']) ]; } } else { diff --git a/src/Appwrite/Specification/Specification.php b/src/Appwrite/Specification/Specification.php index 635cb069bf..73ef34a8b8 100644 --- a/src/Appwrite/Specification/Specification.php +++ b/src/Appwrite/Specification/Specification.php @@ -4,14 +4,8 @@ namespace Appwrite\Specification; class Specification { - /** - * @var Format - */ - protected $format; + protected Format $format; - /** - * @param Format $format - */ public function __construct(Format $format) { $this->format = $format;