Added spec generator CLI command, remove old HTTP endpoint

This commit is contained in:
Eldad Fux 2022-01-01 14:07:11 +02:00
parent 18a2e020c2
commit d1b5f310e3
28 changed files with 335 additions and 249 deletions

View File

@ -259,6 +259,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/realtime && \
chmod +x /usr/local/bin/schedule && \
chmod +x /usr/local/bin/sdks && \
chmod +x /usr/local/bin/specs && \
chmod +x /usr/local/bin/ssl && \
chmod +x /usr/local/bin/test && \
chmod +x /usr/local/bin/vars && \

View File

@ -1,6 +1,6 @@
<?php
require_once __DIR__.'/init.php';
require_once __DIR__.'/controllers/general.php';
use Utopia\App;
use Utopia\CLI\CLI;
@ -13,6 +13,7 @@ include 'tasks/maintenance.php';
include 'tasks/install.php';
include 'tasks/migrate.php';
include 'tasks/sdks.php';
include 'tasks/specs.php';
include 'tasks/ssl.php';
include 'tasks/vars.php';
include 'tasks/usage.php';

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -245,232 +245,6 @@ App::get('/error/:code')
->setParam('body', $page);
});
App::get('/specs/:format')
->groups(['web', 'home'])
->label('scope', 'public')
->label('docs', false)
->label('origin', '*')
->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('tests', 0, function () {return new Range(0, 1);}, 'Include only test services.', true)
->inject('utopia')
->inject('request')
->inject('response')
->action(function ($format, $platform, $tests, $utopia, $request, $response) {
/** @var Utopia\App $utopia */
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
$platforms = [
'client' => APP_PLATFORM_CLIENT,
'server' => APP_PLATFORM_SERVER,
'console' => APP_PLATFORM_CONSOLE,
];
$authCounts = [
'client' => 1,
'server' => 2,
'console' => 1,
];
$routes = [];
$models = [];
$services = [];
$keys = [
APP_PLATFORM_CLIENT => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
],
APP_PLATFORM_SERVER => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'Key' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Key',
'description' => 'Your secret API key',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
],
APP_PLATFORM_CONSOLE => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'Key' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Key',
'description' => 'Your secret API key',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
'Mode' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Mode',
'description' => '',
'in' => 'header',
],
],
];
foreach ($utopia->getRoutes() 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;
}
}
if(empty($routeSecurity)) {
$sdkPlatofrms[] = APP_PLATFORM_CLIENT;
}
if (!$route->getLabel('docs', true)) {
continue;
}
if ($route->getLabel('sdk.mock', false) && !$tests) {
continue;
}
if (!$route->getLabel('sdk.mock', false) && $tests) {
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');
$model = \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']) {
continue;
}
$services[] = [
'name' => $service['key'] ?? '',
'description' => $service['subtitle'] ?? '',
];
}
$models = $response->getModels();
foreach ($models as $key => $value) {
if($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) {
unset($models[$key]);
}
}
switch ($format) {
case 'swagger2':
$format = new Swagger2($utopia, $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0);
break;
case 'open-api3':
$format = new OpenAPI3($utopia, $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0);
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($specs->parse());
});
App::get('/versions')
->desc('Get Version')
->groups(['web', 'home'])

View File

@ -2,8 +2,6 @@
global $cli;
require_once __DIR__.'/../init.php';
use Appwrite\Event\Event;
use Utopia\App;
use Utopia\CLI\Console;

271
app/tasks/specs.php Normal file
View File

@ -0,0 +1,271 @@
<?php
global $cli;
use Utopia\Validator\Text;
use Appwrite\Specification\Format\OpenAPI3;
use Appwrite\Specification\Format\Swagger2;
use Appwrite\Specification\Specification;
use Appwrite\Utopia\Response;
use Swoole\Http\Response as HttpResponse;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Request;
use Utopia\Validator\WhiteList;
$cli
->task('specs')
->param('version', 'latest', new Text(8), 'Spec version', true)
->param('mode', 'normal', new WhiteList(['normal', 'tests']), 'Spec Mode', true)
->action(function ($version, $mode) use ($register) {
$db = $register->get('db');
$redis = $register->get('cache');
$appRoutes = App::getRoutes();
$response = new Response(new HttpResponse());
$tests = ($mode === 'tests');
App::setResource('request', function() {
return new Request;
});
App::setResource('response', function() use ($response) {
return $response;
});
App::setResource('db', fn() => $db);
App::setResource('cache', fn() => $redis);
$platforms = [
'client' => APP_PLATFORM_CLIENT,
'server' => APP_PLATFORM_SERVER,
'console' => APP_PLATFORM_CONSOLE,
];
$authCounts = [
'client' => 1,
'server' => 2,
'console' => 1,
];
$keys = [
APP_PLATFORM_CLIENT => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
],
APP_PLATFORM_SERVER => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'Key' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Key',
'description' => 'Your secret API key',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
],
APP_PLATFORM_CONSOLE => [
'Project' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Project',
'description' => 'Your project ID',
'in' => 'header',
],
'Key' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Key',
'description' => 'Your secret API key',
'in' => 'header',
],
'JWT' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-JWT',
'description' => 'Your secret JSON Web Token',
'in' => 'header',
],
'Locale' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Locale',
'description' => '',
'in' => 'header',
],
'Mode' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Mode',
'description' => '',
'in' => 'header',
],
],
];
foreach (['swagger2', 'open-api3'] as $format) {
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 ($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) && !$tests) {
continue;
}
if (!$route->getLabel('sdk.mock', false) && $tests) {
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');
$model = \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']) {
continue;
}
$services[] = [
'name' => $service['key'] ?? '',
'description' => $service['subtitle'] ?? '',
];
}
$models = $response->getModels();
foreach ($models as $key => $value) {
if($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) {
unset($models[$key]);
}
}
switch ($format) {
case 'swagger2':
$formatInstance = new Swagger2(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0);
break;
case 'open-api3':
$formatInstance = new OpenAPI3(new App('UTC'), $services, $routes, $models, $keys[$platform], $authCounts[$platform] ?? 0);
break;
default:
throw new Exception('Format not found: '.$format);
break;
}
$specs = new Specification($formatInstance);
$endpoint = App::getEnv('_APP_HOME', '[HOSTNAME]');
$email = App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$formatInstance
->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', 'https://HOSTNAME/v1')
->setParam('version', APP_VERSION_STABLE)
->setParam('terms', $endpoint.'/policy/terms')
->setParam('support.email', $email)
->setParam('support.url', $endpoint.'/support')
->setParam('contact.name', APP_NAME.' Team')
->setParam('contact.email', $email)
->setParam('contact.url', $endpoint.'/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', $endpoint.'/docs')
;
if($tests) {
$path = __DIR__.'/../config/specs/'.$format.'-tests-'.$platform.'.json';
if(!file_put_contents($path, json_encode($specs->parse()))) {
throw new Exception('Failed to save tests spec file: '.$path);
}
Console::success('Saved tests spec file: ' . realpath($path));
continue;
}
$path = __DIR__.'/../config/specs/'.$format.'-'.$version.'-'.$platform.'.json';
if(!file_put_contents($path, json_encode($specs->parse()))) {
throw new Exception('Failed to save spec file: '.$path);
}
Console::success('Saved spec file: ' . realpath($path));
}
}
});

View File

@ -2,8 +2,6 @@
global $cli, $register;
require_once __DIR__ . '/../init.php';
use Utopia\App;
use Utopia\Cache\Adapter\Redis;
use Utopia\Cache\Cache;

3
bin/specs Normal file
View File

@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php specs $@

View File

@ -396,7 +396,14 @@ class OpenAPI3 extends Format
foreach ($this->models as $model) {
foreach ($model->getRules() as $rule) {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
if(\is_array($rule['type'])) {
foreach ($rule['type'] as $key => $value) {
$usedModels[] = $value;
}
}
else {
$usedModels[] = $rule['type'];
}
}
}
}

View File

@ -393,7 +393,14 @@ class Swagger2 extends Format
foreach ($this->models as $model) {
foreach ($model->getRules() as $rule) {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
if(\is_array($rule['type'])) {
foreach ($rule['type'] as $key => $value) {
$usedModels[] = $value;
}
}
else {
$usedModels[] = $rule['type'];
}
}
}
}
@ -460,13 +467,13 @@ class Swagger2 extends Format
if(\is_array($rule['type'])) {
if($rule['array']) {
$items = [
'anyOf' => \array_map(function($type) {
'x-anyOf' => \array_map(function($type) {
return ['$ref' => '#/definitions/'.$type];
}, $rule['type'])
];
} else {
$items = [
'oneOf' => \array_map(function($type) {
'x-oneOf' => \array_map(function($type) {
return ['$ref' => '#/definitions/'.$type];
}, $rule['type'])
];
@ -514,7 +521,7 @@ class Swagger2 extends Format
}
}
if (!in_array($name, $required)) {
$output['definitions'][$model->getType()]['properties'][$name]['nullable'] = true;
$output['definitions'][$model->getType()]['properties'][$name]['x-nullable'] = true;
}
}
}

View File

@ -128,26 +128,34 @@ class HTTPTest extends Scope
'content-type' => 'application/json',
], []);
if(!file_put_contents(__DIR__ . '/../../resources/open-api3.json', json_encode($response['body']))) {
throw new Exception('Failed to save spec file');
}
$directory = __DIR__ . '/../../../app/config/specs/';
$files = scandir($directory);
$client = new Client();
$client->setEndpoint('https://validator.swagger.io');
/**
* Test for SUCCESS
*/
$response = $client->call(Client::METHOD_POST, '/validator/debug', [
'content-type' => 'application/json',
], json_decode(file_get_contents(realpath(__DIR__ . '/../../resources/open-api3.json')), true));
foreach($files as $file) {
if(in_array($file, ['.', '..'])) {
continue;
}
$response['body'] = json_decode($response['body'], true);
/**
* Test for SUCCESS
*/
$response = $client->call(Client::METHOD_POST, '/validator/debug', [
'content-type' => 'application/json',
], json_decode(file_get_contents($directory.$file), true));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue(empty($response['body']));
$response['body'] = json_decode($response['body'], true);
unlink(realpath(__DIR__ . '/../../resources/open-api3.json'));
$this->assertEquals(200, $response['headers']['status-code']);
if(!empty($response['body'])){
var_dump($directory.$file);
var_dump($response['body']);
}
$this->assertTrue(empty($response['body']));
}
}
public function testResponseHeader() {