diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 7f7f419708..7043c94e80 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -1,9 +1,14 @@ setParam('body', $page); }, ['layout']); -App::get('/open-api-2.json') +App::get('/specs/:format') ->groups(['web', 'home']) ->label('scope', 'public') ->label('docs', false) + ->param('format', 'swagger2', new WhiteList(['swagger2', 'open-api3'], true), 'Spec format.', true) ->param('platform', APP_PLATFORM_CLIENT, new WhiteList([APP_PLATFORM_CLIENT, APP_PLATFORM_SERVER, APP_PLATFORM_CONSOLE], true), 'Choose target platform.', true) - ->param('extensions', 0, new Range(0, 1), 'Show extra data.', true) - ->param('tests', 0, new Range(0, 1), 'Include only test services.', true) - ->action(function ($platform, $extensions, $tests, $utopia, $request, $response) { + ->action(function ($format, $platform, $utopia, $request, $response) { /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - $security = [ - APP_PLATFORM_CLIENT => ['Project' => []], - APP_PLATFORM_SERVER => ['Project' => [], 'Key' => []], - APP_PLATFORM_CONSOLE => ['Project' => [], 'Key' => []], - ]; - $platforms = [ 'client' => APP_PLATFORM_CLIENT, 'server' => APP_PLATFORM_SERVER, - 'all' => APP_PLATFORM_CONSOLE, + 'console' => APP_PLATFORM_CONSOLE, ]; + $routes = []; + $models = []; + $keys = [ APP_PLATFORM_CLIENT => [ 'Project' => [ @@ -267,98 +268,16 @@ App::get('/open-api-2.json') ], ]; - /* - * Specifications (v3.0.0): - * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md - */ - $output = [ - 'swagger' => '2.0', - 'info' => [ - 'version' => APP_VERSION_STABLE, - 'title' => APP_NAME, - 'description' => 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)', - 'termsOfService' => 'https://appwrite.io/policy/terms', - 'contact' => [ - 'name' => 'Appwrite Team', - 'url' => 'https://appwrite.io/support', - 'email' => App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), - ], - 'license' => [ - 'name' => 'BSD-3-Clause', - 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', - ], - ], - 'host' => \parse_url(App::getEnv('_APP_HOME', $request->getHostname()), PHP_URL_HOST), - 'basePath' => '/v1', - 'schemes' => ['https'], - 'consumes' => ['application/json', 'multipart/form-data'], - 'produces' => ['application/json'], - 'securityDefinitions' => $keys[$platform], - 'paths' => [], - 'definitions' => [ - // 'Pet' => [ - // 'required' => ['id', 'name'], - // 'properties' => [ - // 'id' => [ - // 'type' => 'integer', - // 'format' => 'int64', - // ], - // 'name' => [ - // 'type' => 'string', - // ], - // 'tag' => [ - // 'type' => 'string', - // ], - // ], - // ], - // 'Pets' => array( - // 'type' => 'array', - // 'items' => array( - // '$ref' => '#/definitions/Pet', - // ), - // ), - 'Error' => array( - 'required' => array( - 0 => 'code', - 1 => 'message', - ), - 'properties' => array( - 'code' => array( - 'type' => 'integer', - 'format' => 'int32', - ), - 'message' => array( - 'type' => 'string', - ), - ), - ), - ], - 'externalDocs' => [ - 'description' => 'Full API docs, specs and tutorials', - 'url' => $request->getProtocol().'://'.$request->getHostname().'/docs', - ], + $security = [ + APP_PLATFORM_CLIENT => ['Project' => []], + APP_PLATFORM_SERVER => ['Project' => [], 'Key' => []], + APP_PLATFORM_CONSOLE => ['Project' => [], 'Key' => []], ]; - if ($extensions) { - if (isset($output['securityDefinitions']['Project'])) { - $output['securityDefinitions']['Project']['extensions'] = ['demo' => '5df5acd0d48c2']; - } - - if (isset($output['securityDefinitions']['Key'])) { - $output['securityDefinitions']['Key']['extensions'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; - } - - if (isset($output['securityDefinitions']['Locale'])) { - $output['securityDefinitions']['Locale']['extensions'] = ['demo' => 'en']; - } - - if (isset($output['securityDefinitions']['Mode'])) { - $output['securityDefinitions']['Mode']['extensions'] = ['demo' => '']; - } - } - foreach ($utopia->getRoutes() as $key => $method) { - foreach ($method as $route) { /* @var $route \Utopia\Route */ + foreach ($method as $route) { /** @var \Utopia\Route $route */ + var_dump($route->getURL()); + if (!$route->getLabel('docs', true)) { continue; } @@ -375,184 +294,50 @@ App::get('/open-api-2.json') continue; } - $url = \str_replace('/v1', '', $route->getURL()); - $scope = $route->getLabel('scope', ''); - $hide = $route->getLabel('sdk.hide', false); - $consumes = ['application/json']; - - if ($hide) { - continue; + $routes[] = $route; + $model = $response->getModel($route->getLabel('sdk.response.model', 'none')); + + if($model) { + $models[$model->getType()] = $model; } - - $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__.'/../../../'.$route->getLabel('sdk.description', '')) : null; - - $temp = [ - 'summary' => $route->getDesc(), - 'operationId' => $route->getLabel('sdk.method', \uniqid()), - 'consumes' => [], - 'tags' => [$route->getLabel('sdk.namespace', 'default')], - 'description' => ($desc) ? \file_get_contents($desc) : '', - - // 'responses' => [ - // 200 => [ - // 'description' => 'An paged array of pets', - // 'schema' => [ - // '$ref' => '#/definitions/Pet', - // ], - // ], - // ], - ]; - - if ($extensions) { - $platformList = $route->getLabel('sdk.platform', []); - - $temp['extensions'] = [ - 'weight' => $route->getOrder(), - 'cookies' => $route->getLabel('sdk.cookies', false), - 'type' => $route->getLabel('sdk.methodType', ''), - 'demo' => 'docs/examples/'. Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.Template::fromCamelCaseToDash($temp['operationId']).'.md', - 'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''), - 'rate-limit' => $route->getLabel('abuse-limit', 0), - 'rate-time' => $route->getLabel('abuse-time', 3600), - 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), - 'scope' => $route->getLabel('scope', ''), - 'platforms' => $platformList, - ]; - } - - if ((!empty($scope))) { // && 'public' != $scope - $temp['security'][] = $route->getLabel('sdk.security', $security[$platform]); - } - - $requestBody = [ - 'content' => [ - 'application/x-www-form-urlencoded' => [ - 'schema' => [ - 'type' => 'object', - 'properties' => [], - ], - 'required' => [], - ], - ], - ]; - - foreach ($route->getParams() as $name => $param) { - $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $utopia->getResources($param['resources'])) : $param['validator']; /* @var $validator \Utopia\Validator */ - - $node = [ - 'name' => $name, - 'description' => $param['description'], - 'required' => !$param['optional'], - ]; - - switch ((!empty($validator)) ? \get_class($validator) : '') { - case 'Utopia\Validator\Text': - $node['type'] = 'string'; - $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; - break; - case 'Utopia\Validator\Boolean': - $node['type'] = 'boolean'; - $node['x-example'] = false; - break; - case 'Appwrite\Database\Validator\UID': - $node['type'] = 'string'; - $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; - break; - case 'Utopia\Validator\Email': - $node['type'] = 'string'; - $node['format'] = 'email'; - $node['x-example'] = 'email@example.com'; - break; - case 'Utopia\Validator\URL': - $node['type'] = 'string'; - $node['format'] = 'url'; - $node['x-example'] = 'https://example.com'; - break; - case 'Utopia\Validator\JSON': - case 'Utopia\Validator\Mock': - case 'Utopia\Validator\Assoc': - $node['type'] = 'object'; - $node['type'] = 'object'; - $node['x-example'] = '{}'; - //$node['format'] = 'json'; - break; - case 'Appwrite\Storage\Validator\File': - $consumes = ['multipart/form-data']; - $node['type'] = 'file'; - break; - case 'Utopia\Validator\ArrayList': - $node['type'] = 'array'; - $node['collectionFormat'] = 'multi'; - $node['items'] = [ - 'type' => 'string', - ]; - break; - case 'Appwrite\Auth\Validator\Password': - $node['type'] = 'string'; - $node['format'] = 'format'; - $node['x-example'] = 'password'; - break; - case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ - $node['type'] = 'integer'; - $node['format'] = 'int32'; - $node['x-example'] = $validator->getMin(); - break; - case 'Utopia\Validator\Numeric': - $node['type'] = 'integer'; - $node['format'] = 'int32'; - break; - case 'Utopia\Validator\Length': - $node['type'] = 'string'; - break; - case 'Utopia\Validator\Host': - $node['type'] = 'string'; - $node['format'] = 'url'; - $node['x-example'] = 'https://example.com'; - break; - case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ - $node['type'] = 'string'; - $node['x-example'] = $validator->getList()[0]; - break; - default: - $node['type'] = 'string'; - break; - } - - if ($param['optional'] && !\is_null($param['default'])) { // Param has default value - $node['default'] = $param['default']; - } - - if (false !== \strpos($url, ':'.$name)) { // Param is in URL path - $node['in'] = 'path'; - $temp['parameters'][] = $node; - } elseif ($key == 'GET') { // Param is in query - $node['in'] = 'query'; - $temp['parameters'][] = $node; - } else { // Param is in payload - $node['in'] = 'formData'; - $temp['parameters'][] = $node; - $requestBody['content']['application/x-www-form-urlencoded']['schema']['properties'][] = $node; - - if (!$param['optional']) { - $requestBody['content']['application/x-www-form-urlencoded']['required'][] = $name; - } - } - - $url = \str_replace(':'.$name, '{'.$name.'}', $url); - } - - $temp['consumes'] = $consumes; - - $output['paths'][$url][\strtolower($route->getMethod())] = $temp; } } - /*foreach ($consoleDB->getMocks() as $mock) { - var_dump($mock['name']); - }*/ + $models = $response->getModels(); - \ksort($output['paths']); + switch ($format) { + case 'swagger2': + $format = new Swagger2($utopia, $routes, $models, $keys[$platform], $security[$platform]); + break; + + case 'open-api3': + $format = new OpenAPI3($utopia, $routes, $models, $keys[$platform], $security[$platform]); + break; + + default: + throw new Exception('Format not found', 404); + break; + } + + $specs = new Specification($format); + + $format + ->setParam('name', APP_NAME) + ->setParam('description', 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)') + ->setParam('endpoint', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/v1') + ->setParam('version', APP_VERSION_STABLE) + ->setParam('terms', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/policy/terms') + ->setParam('support.email', App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM)) + ->setParam('support.url', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/support') + ->setParam('contact.name', APP_NAME.' Team') + ->setParam('contact.email', App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM)) + ->setParam('contact.url', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/support') + ->setParam('license.name', 'BSD-3-Clause') + ->setParam('license.url', 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE') + ->setParam('docs.description', 'Full API docs, specs and tutorials') + ->setParam('docs.url', App::getEnv('_APP_HOME', $request->getProtocol().'://'.$request->getHostname()).'/docs') + ; $response - ->json($output); + ->json($specs->parse()); }, ['utopia', 'request', 'response']); \ No newline at end of file diff --git a/src/Appwrite/Specification/Format.php b/src/Appwrite/Specification/Format.php new file mode 100644 index 0000000000..6f2d154e48 --- /dev/null +++ b/src/Appwrite/Specification/Format.php @@ -0,0 +1,124 @@ + '', + 'description' => '', + 'endpoint' => 'https://localhost', + 'version' => '1.0.0', + 'terms' => '', + 'support.email' => '', + 'support.url' => '', + 'contact.name' => '', + 'contact.email' => '', + 'contact.url' => '', + 'license.name' => '', + 'license.url' => '', + ]; + + /** + * @param App $app + * @param Route[] $routes + * @param Model[] $models + * @param array $keys + * @param array $security + */ + public function __construct(App $app, array $routes, array $models, array $keys, array $security) + { + $this->app = $app; + $this->routes = $routes; + $this->models = $models; + $this->keys = $keys; + $this->security = $security; + } + + /** + * Get Name. + * + * Get format name + * + * @return string + */ + abstract public function getName(): string; + + /** + * Parse + * + * Parses Appwrite App to given format + * + * @return array + */ + abstract public function parse(): array; + + /** + * Set Param. + * + * Set param value + * + * @param string $key + * @param string $value + * + * @return self + */ + public function setParam(string $key, string $value): self + { + $this->params[$key] = $value; + + return $this; + } + + /** + * Get Param. + * + * Get param value + * + * @param string $key + * @param string $default + * + * @return string + */ + public function getParam(string $key, string $default = ''): string + { + if(!isset($this->params[$key])) { + return $default; + } + + return $this->params[$key]; + } + +} diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php new file mode 100644 index 0000000000..847b2a3e94 --- /dev/null +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -0,0 +1,340 @@ + '3.0.0', + 'info' => [ + 'version' => $this->getParam('version'), + 'title' => $this->getParam('name'), + 'description' => $this->getParam('description'), + 'termsOfService' => $this->getParam('terms'), + 'contact' => [ + 'name' => $this->getParam('contact.name'), + 'url' => $this->getParam('contact.url'), + 'email' => $this->getParam('contact.email'), + ], + 'license' => [ + 'name' => 'BSD-3-Clause', + 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', + ], + ], + 'servers' => [ + [ + 'url' => $this->getParam('endpoint', ''), + ], + ], + 'paths' => [], + 'components' => [ + 'schemas' => [], + 'securitySchemes' => $this->security, + ], + 'externalDocs' => [ + 'description' => $this->getParam('docs.description'), + 'url' => $this->getParam('docs.url'), + ], + ]; + + if (isset($output['components']['securitySchemes']['Project'])) { + $output['components']['securitySchemes']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2']; + } + + if (isset($output['components']['securitySchemes']['Key'])) { + $output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; + } + + if (isset($output['components']['securitySchemes']['Locale'])) { + $output['components']['securitySchemes']['Locale']['x-appwrite'] = ['demo' => 'en']; + } + + if (isset($output['components']['securitySchemes']['Mode'])) { + $output['components']['securitySchemes']['Mode']['x-appwrite'] = ['demo' => '']; + } + + foreach ($this->routes as $route) { /* @var $route \Utopia\Route */ + $url = \str_replace('/v1', '', $route->getURL()); + $scope = $route->getLabel('scope', ''); + $hide = $route->getLabel('sdk.hide', false); + + if ($hide) { + continue; + } + + $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__.'/../../../'.$route->getLabel('sdk.description', '')) : null; + + $model = $route->getLabel('sdk.response.model', 'none'); + + $temp = [ + + 'summary' => $route->getDesc(), + 'operationId' => $route->getLabel('sdk.method', \uniqid()), + 'tags' => [$route->getLabel('sdk.namespace', 'default')], + 'description' => ($desc) ? \file_get_contents($desc) : '', + 'responses' => [ + (string)$route->getLabel('sdk.response.code', '500') => [ + 'description' => '', + 'content' => [ + $route->getLabel('sdk.response.type', '') => [ + 'schema' => [ + '$ref' => '#/components/schemas/'.$model, + ], + ] + ], + ], + ], + 'requestBody' => [ + 'content' => [ + 'multipart/form-data' => [ + 'schema' => [ + 'type' => 'object', + ], + ], + ] + ], + 'x-appwrite' => [ // Appwrite related metadata + 'weight' => $route->getOrder(), + 'cookies' => $route->getLabel('sdk.cookies', false), + 'type' => $route->getLabel('sdk.methodType', ''), + 'demo' => 'docs/examples/'. Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.Template::fromCamelCaseToDash($temp['operationId']).'.md', + 'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''), + 'rate-limit' => $route->getLabel('abuse-limit', 0), + 'rate-time' => $route->getLabel('abuse-time', 3600), + 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), + 'scope' => $route->getLabel('scope', ''), + 'platforms' => $route->getLabel('sdk.platform', []), + ], + ]; + + if ((!empty($scope))) { // && 'public' != $scope + $temp['security'][] = $route->getLabel('sdk.security', $this->security); + } + + foreach ($route->getParams() as $name => $param) { + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['resources'])) : $param['validator']; /* @var $validator \Utopia\Validator */ + + $node = [ + 'name' => $name, + 'description' => $param['description'], + 'required' => !$param['optional'], + ]; + + switch ((!empty($validator)) ? \get_class($validator) : '') { + case 'Utopia\Validator\Text': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; + break; + case 'Utopia\Validator\Boolean': + $node['type'] = 'boolean'; + $node['x-example'] = false; + break; + case 'Appwrite\Database\Validator\UID': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; + break; + case 'Utopia\Validator\Email': + $node['type'] = 'string'; + $node['format'] = 'email'; + $node['x-example'] = 'email@example.com'; + break; + case 'Utopia\Validator\URL': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\JSON': + case 'Utopia\Validator\Mock': + case 'Utopia\Validator\Assoc': + $node['type'] = 'object'; + $node['type'] = 'object'; + $node['x-example'] = '{}'; + //$node['format'] = 'json'; + break; + case 'Appwrite\Storage\Validator\File': + $requestType = 'multipart/form-data'; + $node['type'] = 'file'; + break; + case 'Utopia\Validator\ArrayList': + $node['type'] = 'array'; + $node['collectionFormat'] = 'multi'; + $node['items'] = [ + 'type' => 'string', + ]; + break; + case 'Appwrite\Auth\Validator\Password': + $node['type'] = 'string'; + $node['format'] = 'format'; + $node['x-example'] = 'password'; + break; + case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ + $node['type'] = 'integer'; + $node['format'] = 'int32'; + $node['x-example'] = $validator->getMin(); + break; + case 'Utopia\Validator\Numeric': + $node['type'] = 'integer'; + $node['format'] = 'int32'; + break; + case 'Utopia\Validator\Length': + $node['type'] = 'string'; + break; + case 'Utopia\Validator\Host': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ + $node['type'] = 'string'; + $node['x-example'] = $validator->getList()[0]; + break; + default: + $node['type'] = 'string'; + break; + } + + if ($param['optional'] && !\is_null($param['default'])) { // Param has default value + $node['default'] = $param['default']; + } + + if (false !== \strpos($url, ':'.$name)) { // Param is in URL path + $node['in'] = 'path'; + $temp['parameters'][] = $node; + } elseif ($route->getMethod() == 'GET') { // Param is in query + $node['in'] = 'query'; + $temp['parameters'][] = $node; + } else { // Param is in payload + $node['in'] = 'formData'; + $temp['parameters'][] = $node; + $requestBody['content']['application/x-www-form-urlencoded']['schema']['properties'][] = $node; + + if (!$param['optional']) { + $requestBody['content']['application/x-www-form-urlencoded']['required'][] = $name; + } + } + + $url = \str_replace(':'.$name, '{'.$name.'}', $url); + } + + $output['paths'][$url][\strtolower($route->getMethod())] = $temp; + } + + foreach ($this->models as $model) { + $required = $model->getRequired(); + $rules = $model->getRules(); + + $output['components']['schemas'][$model->getType()] = [ + 'type' => 'object', + 'properties' => (empty($rules)) ? new \stdClass : [], + ]; + + if($model->isAny()) { + $output['components']['schemas'][$model->getType()]['additionalProperties'] = true; + } + + if(!empty($required)) { + $output['components']['schemas'][$model->getType()]['required'] = $required; + } + + foreach($model->getRules() as $name => $rule) { + $type = ''; + $format = null; + $items = null; + + switch ($rule['type']) { + case 'string': + case 'json': + $type = 'string'; + break; + + case 'integer': + $type = 'integer'; + $format = 'int32'; + break; + + case 'float': + $type = 'number'; + $format = 'float'; + break; + + case 'boolean': + $type = 'string'; + break; + + default: + $type = 'object'; + $rule['type'] = ($rule['type']) ? $rule['type'] : 'none'; + + $items = [ + 'type' => $type, + '$ref' => '#/components/schemas/'.$rule['type'], + ]; + break; + } + + if($rule['array']) { + $output['components']['schemas'][$model->getType()]['properties'][$name] = [ + 'type' => 'array', + 'description' => $rule['description'] ?? '', + 'items' => [ + 'type' => $type, + ] + ]; + + if($format) { + $output['components']['schemas'][$model->getType()]['properties'][$name]['items']['format'] = $format; + } + + if($items) { + $output['components']['schemas'][$model->getType()]['properties'][$name]['items'] = $items; + } + } else { + $output['components']['schemas'][$model->getType()]['properties'][$name] = [ + 'type' => $type, + 'description' => $rule['description'] ?? '', + ]; + + if($format) { + $output['components']['schemas'][$model->getType()]['properties'][$name]['format'] = $format; + } + + if($items) { + $output['components']['schemas'][$model->getType()]['properties'][$name]['items'] = $items; + } + } + } + } + + \ksort($output['paths']); + + return $output; + } +} diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php new file mode 100644 index 0000000000..ffd089109c --- /dev/null +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -0,0 +1,393 @@ + '2.0', + 'info' => [ + 'version' => $this->getParam('version'), + 'title' => $this->getParam('name'), + 'description' => $this->getParam('description'), + 'termsOfService' => $this->getParam('terms'), + 'contact' => [ + 'name' => $this->getParam('contact.name'), + 'url' => $this->getParam('contact.url'), + 'email' => $this->getParam('contact.email'), + ], + 'license' => [ + 'name' => 'BSD-3-Clause', + 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', + ], + ], + 'host' => \parse_url($this->getParam('endpoint', ''), PHP_URL_HOST), + 'basePath' => \parse_url($this->getParam('endpoint', ''), PHP_URL_PATH), + 'schemes' => [\parse_url($this->getParam('endpoint', ''), PHP_URL_SCHEME)], + 'consumes' => ['application/json', 'multipart/form-data'], + 'produces' => ['application/json'], + 'securityDefinitions' => $this->keys, + 'paths' => [], + 'definitions' => [], + 'externalDocs' => [ + 'description' => $this->getParam('docs.description'), + 'url' => $this->getParam('docs.url'), + ], + ]; + + if (isset($output['securityDefinitions']['Project'])) { + $output['securityDefinitions']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2']; + } + + if (isset($output['securityDefinitions']['Key'])) { + $output['securityDefinitions']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2']; + } + + if (isset($output['securityDefinitions']['Locale'])) { + $output['securityDefinitions']['Locale']['x-appwrite'] = ['demo' => 'en']; + } + + if (isset($output['securityDefinitions']['Mode'])) { + $output['securityDefinitions']['Mode']['x-appwrite'] = ['demo' => '']; + } + + foreach ($this->routes as $route) { /* @var $route \Utopia\Route */ + $url = \str_replace('/v1', '', $route->getURL()); + $scope = $route->getLabel('scope', ''); + $hide = $route->getLabel('sdk.hide', false); + $consumes = [$route->getLabel('sdk.request.type', 'application/json')]; + + if ($hide) { + continue; + } + + $id = $route->getLabel('sdk.method', \uniqid()); + $desc = (!empty($route->getLabel('sdk.description', ''))) ? \realpath(__DIR__.'/../../../../'.$route->getLabel('sdk.description', '')) : null; + $produces = $route->getLabel('sdk.response.type', 'application/json'); + $model = $route->getLabel('sdk.response.model', 'none'); + + $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' => [], + 'x-appwrite' => [ // Appwrite related metadata + 'method' => $route->getLabel('sdk.method', \uniqid()), + 'weight' => $route->getOrder(), + 'cookies' => $route->getLabel('sdk.cookies', false), + 'type' => $route->getLabel('sdk.methodType', ''), + 'demo' => 'docs/examples/'. Template::fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.Template::fromCamelCaseToDash($id).'.md', + 'edit' => 'https://github.com/appwrite/appwrite/edit/master' . $route->getLabel('sdk.description', ''), + 'rate-limit' => $route->getLabel('abuse-limit', 0), + 'rate-time' => $route->getLabel('abuse-time', 3600), + 'rate-key' => $route->getLabel('abuse-key', 'url:{url},ip:{ip}'), + 'scope' => $route->getLabel('scope', ''), + 'platforms' => $route->getLabel('sdk.platform', []), + ], + ]; + + foreach ($this->models as $key => $value) { + if($value->getType() === $model) { + $model = $value; + break; + } + } + + if($model->isNone()) { + $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [ + 'description' => (in_array($produces, [ + 'image/*', + 'image/jpeg', + 'image/gif', + 'image/png', + 'image/webp', + 'image/svg-x', + 'image/x-icon', + 'image/bmp', + ])) ? 'Image' : 'File', + 'schema' => [ + 'type' => 'file' + ], + ]; + } else { + $temp['responses'][(string)$route->getLabel('sdk.response.code', '500')] = [ + 'description' => $model->getName(), + 'schema' => [ + '$ref' => '#/definitions/'.$model->getType(), + ], + ]; + } + + if ((!empty($scope))) { // && 'public' != $scope + $temp['security'][] = $route->getLabel('sdk.security', $this->security); + } + + $body = [ + 'name' => 'payload', + 'in' => 'body', + 'schema' => [ + 'type' => 'object', + 'properties' => [], + ], + ]; + + $bodyRequired = []; + + foreach ($route->getParams() as $name => $param) { // Set params + $validator = (\is_callable($param['validator'])) ? call_user_func_array($param['validator'], $this->app->getResources($param['resources'])) : $param['validator']; /* @var $validator \Utopia\Validator */ + + $node = [ + 'name' => $name, + 'description' => $param['description'], + 'required' => !$param['optional'], + ]; + + switch ((!empty($validator)) ? \get_class($validator) : '') { + case 'Utopia\Validator\Text': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; + break; + case 'Utopia\Validator\Boolean': + $node['type'] = 'boolean'; + $node['x-example'] = false; + break; + case 'Appwrite\Database\Validator\UID': + $node['type'] = 'string'; + $node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']'; + break; + case 'Utopia\Validator\Email': + $node['type'] = 'string'; + $node['format'] = 'email'; + $node['x-example'] = 'email@example.com'; + break; + case 'Utopia\Validator\URL': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\JSON': + case 'Utopia\Validator\Mock': + case 'Utopia\Validator\Assoc': + $node['type'] = 'object'; + $node['type'] = 'object'; + $node['x-example'] = '{}'; + //$node['format'] = 'json'; + break; + case 'Appwrite\Storage\Validator\File': + $consumes = ['multipart/form-data']; + $node['type'] = 'file'; + break; + case 'Utopia\Validator\ArrayList': + $node['type'] = 'array'; + $node['collectionFormat'] = 'multi'; + $node['items'] = [ + 'type' => 'string', + ]; + break; + case 'Appwrite\Auth\Validator\Password': + $node['type'] = 'string'; + $node['format'] = 'format'; + $node['x-example'] = 'password'; + break; + case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ + $node['type'] = 'integer'; + $node['format'] = 'int32'; + $node['x-example'] = $validator->getMin(); + break; + case 'Utopia\Validator\Numeric': + $node['type'] = 'integer'; + $node['format'] = 'int32'; + break; + case 'Utopia\Validator\Length': + $node['type'] = 'string'; + break; + case 'Utopia\Validator\Host': + $node['type'] = 'string'; + $node['format'] = 'url'; + $node['x-example'] = 'https://example.com'; + break; + case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ + $node['type'] = 'string'; + $node['x-example'] = $validator->getList()[0]; + break; + default: + $node['type'] = 'string'; + break; + } + + if ($param['optional'] && !\is_null($param['default'])) { // Param has default value + $node['default'] = $param['default']; + } + + if (false !== \strpos($url, ':'.$name)) { // Param is in URL path + $node['in'] = 'path'; + $temp['parameters'][] = $node; + } elseif ($route->getMethod() == 'GET') { // Param is in query + $node['in'] = 'query'; + $temp['parameters'][] = $node; + } else { // Param is in payload + + if(\in_array('multipart/form-data', $consumes)) { + $node['in'] = 'formData'; + $temp['parameters'][] = $node; + + continue; + } + + if(!$param['optional']) { + $bodyRequired[] = $name; + } + + $body['schema']['properties'][$name] = [ + 'type' => $node['type'], + 'description' => $node['description'], + 'default' => $node['default'] ?? null, + 'x-example' => $node['x-example'] ?? null, + ]; + + if(\array_key_exists('items', $node)) { + $body['schema']['properties'][$name]['items'] = $node['items']; + } + } + + $url = \str_replace(':'.$name, '{'.$name.'}', $url); + } + + if(!empty($body['schema']['properties'])) { + $temp['parameters'][] = $body; + } + + if(!empty($bodyRequired)) { + $body['schema']['required'] = $bodyRequired; + } + + $temp['consumes'] = $consumes; + + $output['paths'][$url][\strtolower($route->getMethod())] = $temp; + } + + foreach ($this->models as $model) { + $required = $model->getRequired(); + $rules = $model->getRules(); + + $output['definitions'][$model->getType()] = [ + 'type' => 'object', + ]; + + if(!empty($rules)) { + $output['definitions'][$model->getType()]['properties'] = []; + } + + if($model->isAny()) { + $output['definitions'][$model->getType()]['additionalProperties'] = true; + } + + if(!empty($required)) { + $output['definitions'][$model->getType()]['required'] = $required; + } + + foreach($model->getRules() as $name => $rule) { + $type = ''; + $format = null; + $items = null; + + switch ($rule['type']) { + case 'string': + case 'json': + $type = 'string'; + break; + + case 'integer': + $type = 'integer'; + $format = 'int32'; + break; + + case 'float': + $type = 'number'; + $format = 'float'; + break; + + case 'boolean': + $type = 'string'; + break; + + default: + $type = 'object'; + $rule['type'] = ($rule['type']) ? $rule['type'] : 'none'; + + $items = [ + 'type' => $type, + '$ref' => '#/definitions/'.$rule['type'], + ]; + break; + } + + if($rule['array']) { + $output['definitions'][$model->getType()]['properties'][$name] = [ + 'type' => 'array', + 'description' => $rule['description'] ?? '', + 'items' => [ + 'type' => $type, + ] + ]; + + if($format) { + $output['definitions'][$model->getType()]['properties'][$name]['items']['format'] = $format; + } + + if($items) { + $output['definitions'][$model->getType()]['properties'][$name]['items'] = $items; + } + } else { + $output['definitions'][$model->getType()]['properties'][$name] = [ + 'type' => $type, + 'description' => $rule['description'] ?? '', + ]; + + if($format) { + $output['definitions'][$model->getType()]['properties'][$name]['format'] = $format; + } + + if($items) { + $output['definitions'][$model->getType()]['properties'][$name]['items'] = $items; + } + } + } + } + + \ksort($output['paths']); + + return $output; + } +} diff --git a/src/Appwrite/Specification/Specification.php b/src/Appwrite/Specification/Specification.php new file mode 100644 index 0000000000..d28ddd8be6 --- /dev/null +++ b/src/Appwrite/Specification/Specification.php @@ -0,0 +1,43 @@ +format = $format; + } + + /** + * Get Name. + * + * Get format name + * + * @return string + */ + public function getName():string + { + return $this->format->getName(); + } + + /** + * Parse + * + * Parses Appwrite App to given format + * + * @return array + */ + public function parse(): array + { + return $this->format->parse(); + } +} \ No newline at end of file