Handle generating request model specs

This commit is contained in:
Jake Barnby
2025-12-09 00:30:53 +13:00
parent 70d59ad5c6
commit 9ed2ebd1a5
5 changed files with 298 additions and 96 deletions
+21 -8
View File
@@ -8,6 +8,7 @@ use Appwrite\SDK\Specification\Format\OpenAPI3;
use Appwrite\SDK\Specification\Format\Swagger2;
use Appwrite\SDK\Specification\Specification;
use Appwrite\Utopia\Request as AppwriteRequest;
use Appwrite\Utopia\Request\Model as RequestModel;
use Appwrite\Utopia\Response as AppwriteResponse;
use Exception;
use Swoole\Http\Request as SwooleRequest;
@@ -192,7 +193,8 @@ class Specs extends Action
foreach ($platforms as $platform) {
$routes = [];
$models = [];
$requestModels = [];
$responseModels = [];
$services = [];
foreach ($appRoutes as $key => $method) {
@@ -264,9 +266,9 @@ class Specs extends Action
foreach (Config::getParam('services', []) as $service) {
if (
!isset($service['docs']) // Skip service if not part of the public API
|| !isset($service['sdk'])
!isset($service['docs']) // Skip service if not part of the public API
|| !$service['docs']
|| !isset($service['sdk'])
|| !$service['sdk']
) {
continue;
@@ -278,11 +280,21 @@ class Specs extends Action
];
}
$models = $response->getModels();
$responseModels = $response->getModels();
foreach ($models as $key => $value) {
foreach ($responseModels as $key => $value) {
if ($platform !== APP_PLATFORM_CONSOLE && !$value->isPublic()) {
unset($models[$key]);
unset($responseModels[$key]);
}
}
foreach ($routes as $route) {
foreach ($route->getParams() as $param) {
$model = $param['model'] ?? null;
if ($model !== null && \class_exists($model)) {
$instance = new $model();
$requestModels[$instance->getType()] = $instance;
}
}
}
@@ -290,10 +302,11 @@ class Specs extends Action
new App('UTC'),
$services,
$routes,
$models,
$requestModels,
$responseModels,
$keys[$platform],
$authCounts[$platform] ?? 0,
$platforms[$platform]
$platforms[$platform],
];
foreach (['swagger2', 'open-api3'] as $format) {
+32 -26
View File
@@ -2,6 +2,7 @@
namespace Appwrite\SDK\Specification;
use Appwrite\Utopia\Request\Model as RequestModel;
use Appwrite\Utopia\Response\Model;
use Utopia\App;
use Utopia\Config\Config;
@@ -9,22 +10,6 @@ use Utopia\Route;
abstract class Format
{
protected App $app;
/**
* @var array<Route>
*/
protected array $routes;
/**
* @var array<Model>
*/
protected array $models;
protected array $services;
protected array $keys;
protected int $authCount;
protected string $platform;
protected array $params = [
'name' => '',
'description' => '',
@@ -51,15 +36,16 @@ abstract class Format
]
];
public function __construct(App $app, array $services, array $routes, array $models, array $keys, int $authCount, string $platform)
{
$this->app = $app;
$this->services = $services;
$this->routes = $routes;
$this->models = $models;
$this->keys = $keys;
$this->authCount = $authCount;
$this->platform = $platform;
public function __construct(
protected App $app,
protected array $services,
protected array $routes,
protected array $requestModels,
protected array $responseModels,
protected array $keys,
protected int $authCount,
protected string $platform,
) {
}
/**
@@ -740,7 +726,7 @@ abstract class Format
foreach ($types as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
foreach ($this->responseModels as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
}
@@ -749,4 +735,24 @@ abstract class Format
}
}
}
protected function getNestedRequestModels(RequestModel $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (!in_array($model->getType(), $usedModels)) {
continue;
}
$types = (array)$rule['type'];
foreach ($types as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float', 'double', 'datetime', 'payload', 'array', 'enum'])) {
$usedModels[] = $ruleType;
foreach ($this->requestModels as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedRequestModels($m, $usedModels);
}
}
}
}
}
}
}
@@ -9,6 +9,7 @@ use Appwrite\SDK\Response;
use Appwrite\SDK\Specification\Format;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\Operation;
use Appwrite\Utopia\Request\Model as RequestModel;
use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\Any;
use Utopia\Database\Database;
@@ -91,6 +92,7 @@ class OpenAPI3 extends Format
}
$usedModels = [];
$usedRequestModels = [];
foreach ($this->routes as $route) {
$url = \str_replace('/v1', '', $route->getPath());
@@ -259,7 +261,7 @@ class OpenAPI3 extends Format
if (\is_array($responseModel)) {
foreach ($responseModel as $modelName) {
foreach ($this->models as $value) {
foreach ($this->responseModels as $value) {
if ($value->getType() === $modelName) {
$usedModels[] = $modelName;
break;
@@ -278,7 +280,7 @@ class OpenAPI3 extends Format
// lets not assume stuff here!
if ($response->getCode() !== 204) {
$responseData['model'] = '#/components/schemas/' . $responseModel;
foreach ($this->models as $value) {
foreach ($this->responseModels as $value) {
if ($value->getType() === $responseModel) {
$usedModels[] = $responseModel;
break;
@@ -299,7 +301,7 @@ class OpenAPI3 extends Format
/** @var Response $response */
$model = $response->getModel();
foreach ($this->models as $value) {
foreach ($this->responseModels as $value) {
if (\is_array($model)) {
$model = \array_map(fn ($m) => $m === $value->getType() ? $value : $m, $model);
} else {
@@ -719,37 +721,50 @@ class OpenAPI3 extends Format
$bodyRequired[] = $name;
}
$body['content'][$consumes[0]]['schema']['properties'][$name] = [
'type' => $node['schema']['type'],
'description' => $node['description'],
'x-example' => $node['schema']['x-example'] ?? null
];
$property = &$body['content'][$consumes[0]]['schema']['properties'][$name];
$paramModel = $param['model'] ?? null;
if (isset($node['schema']['enum'])) {
/// If the enum flag is Set, add the enum values to the body
$body['content'][$consumes[0]]['schema']['properties'][$name]['enum'] = $node['schema']['enum'];
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-enum-name'] = $node['schema']['x-enum-name'] ?? null;
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-enum-keys'] = $node['schema']['x-enum-keys'] ?? null;
}
if ($paramModel !== null && \class_exists($paramModel)) {
/** @var RequestModel $requestModelInstance */
$requestModelInstance = new $paramModel();
$requestModelType = $requestModelInstance->getType();
$usedRequestModels[] = $requestModelType;
if ($node['schema']['x-upload-id'] ?? false) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-upload-id'] = $node['schema']['x-upload-id'];
}
$property = $array
? ['type' => 'array', 'description' => $node['description'], 'items' => ['$ref' => '#/components/schemas/' . $requestModelType]]
: ['$ref' => '#/components/schemas/' . $requestModelType];
} else {
$property = [
'type' => $node['schema']['type'],
'description' => $node['description'],
'x-example' => $node['schema']['x-example'] ?? null
];
if (isset($node['default'])) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['default'] = $node['default'];
}
if (isset($node['schema']['enum'])) {
$property['enum'] = $node['schema']['enum'];
$property['x-enum-name'] = $node['schema']['x-enum-name'] ?? null;
$property['x-enum-keys'] = $node['schema']['x-enum-keys'] ?? null;
}
if (\array_key_exists('items', $node['schema'])) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['items'] = $node['schema']['items'];
}
if ($node['schema']['x-upload-id'] ?? false) {
$property['x-upload-id'] = $node['schema']['x-upload-id'];
}
if ($node['x-global'] ?? false) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-global'] = true;
if (isset($node['default'])) {
$property['default'] = $node['default'];
}
if (\array_key_exists('items', $node['schema'])) {
$property['items'] = $node['schema']['items'];
}
if ($node['x-global'] ?? false) {
$property['x-global'] = true;
}
}
if ($isNullable) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-nullable'] = true;
$property['nullable'] = true;
}
}
@@ -767,11 +782,11 @@ class OpenAPI3 extends Format
$output['paths'][$url][\strtolower($route->getMethod())] = $temp;
}
foreach ($this->models as $model) {
foreach ($this->responseModels as $model) {
$this->getNestedModels($model, $usedModels);
}
foreach ($this->models as $model) {
foreach ($this->responseModels as $model) {
if (!in_array($model->getType(), $usedModels) && $model->getType() !== 'error') {
continue;
}
@@ -931,6 +946,86 @@ class OpenAPI3 extends Format
$output['components']['schemas'][$model->getType()]['example'] = $examples;
}
// Generate request model schemas
foreach ($this->requestModels as $model) {
$this->getNestedRequestModels($model, $usedRequestModels);
}
foreach ($this->requestModels as $model) {
if (!in_array($model->getType(), $usedRequestModels)) {
continue;
}
$required = $model->getRequired();
$rules = $model->getRules();
$output['components']['schemas'][$model->getType()] = [
'description' => $model->getName(),
'type' => 'object',
];
if (!empty($rules)) {
$output['components']['schemas'][$model->getType()]['properties'] = [];
}
if (!empty($required)) {
$output['components']['schemas'][$model->getType()]['required'] = $required;
}
foreach ($rules as $name => $rule) {
$ruleType = $rule['type'];
$isArray = $rule['array'] ?? false;
[$type, $format] = match ($ruleType) {
'string', 'datetime', 'payload' => ['string', null],
'enum' => ['string', null],
'json' => ['object', null],
'array' => ['array', null],
'integer' => ['integer', 'int32'],
'float' => ['number', 'float'],
'double' => ['number', 'double'],
'boolean' => ['boolean', null],
default => ['object', null],
};
$property = &$output['components']['schemas'][$model->getType()]['properties'][$name];
$property = ['type' => $isArray ? 'array' : $type, 'description' => $rule['description'] ?? '', 'x-example' => $rule['example'] ?? null];
if ($ruleType === 'json') {
$property['additionalProperties'] = true;
}
if ($isArray) {
$property['items'] = ['type' => $type];
if ($format) {
$property['items']['format'] = $format;
}
} elseif ($format) {
$property['format'] = $format;
}
// Handle nested model references
if ($type === 'object' && $ruleType && $ruleType !== 'json') {
$refKey = $isArray ? 'anyOf' : 'oneOf';
$property['items'] = \is_array($ruleType)
? [$refKey => \array_map(fn ($t) => ['$ref' => '#/components/schemas/' . $t], $ruleType)]
: ['$ref' => '#/components/schemas/' . $ruleType];
}
if ($ruleType === 'enum' && !empty($rule['enum'])) {
$target = $isArray ? $property['items'] : $property;
$target['enum'] = \array_values($rule['enum']);
$isArray ? $property['items'] = $target : $property = $target;
}
if (!in_array($name, $required)) {
$property['nullable'] = true;
}
}
$output['components']['schemas'][$model->getType()]['example'] = \array_map(fn ($r) => $r['example'] ?? null, $rules);
}
\ksort($output['paths']);
return $output;
@@ -9,6 +9,7 @@ use Appwrite\SDK\Response;
use Appwrite\SDK\Specification\Format;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\Operation;
use Appwrite\Utopia\Request\Model as RequestModel;
use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\Any;
use Utopia\Database\Database;
@@ -88,6 +89,7 @@ class Swagger2 extends Format
}
$usedModels = [];
$usedRequestModels = [];
foreach ($this->routes as $route) {
/** @var Route $route */
@@ -267,7 +269,7 @@ class Swagger2 extends Format
$responseModel = $response->getModel();
if (\is_array($responseModel)) {
foreach ($responseModel as $modelName) {
foreach ($this->models as $value) {
foreach ($this->responseModels as $value) {
if ($value->getType() === $modelName) {
$usedModels[] = $modelName;
break;
@@ -286,7 +288,7 @@ class Swagger2 extends Format
// lets not assume stuff here!
if ($response->getCode() !== 204) {
$responseData['model'] = '#/definitions/' . $responseModel;
foreach ($this->models as $value) {
foreach ($this->responseModels as $value) {
if ($value->getType() === $responseModel) {
$usedModels[] = $responseModel;
break;
@@ -307,7 +309,7 @@ class Swagger2 extends Format
/** @var Response $response */
$model = $response->getModel();
foreach ($this->models as $value) {
foreach ($this->responseModels as $value) {
if (\is_array($model)) {
$model = \array_map(fn ($m) => $m === $value->getType() ? $value : $m, $model);
} else {
@@ -707,30 +709,43 @@ class Swagger2 extends Format
$bodyRequired[] = $name;
}
$body['schema']['properties'][$name] = [
'type' => $node['type'],
'description' => $node['description'],
'default' => $node['default'] ?? null,
'x-example' => $node['x-example'] ?? null,
];
$property = &$body['schema']['properties'][$name];
$paramModel = $param['model'] ?? null;
if (isset($node['enum'])) {
/// If the enum flag is Set, add the enum values to the body
$body['schema']['properties'][$name]['enum'] = $node['enum'];
$body['schema']['properties'][$name]['x-enum-name'] = $node['x-enum-name'] ?? null;
$body['schema']['properties'][$name]['x-enum-keys'] = $node['x-enum-keys'] ?? null;
}
if ($paramModel !== null && \class_exists($paramModel)) {
/** @var RequestModel $requestModelInstance */
$requestModelInstance = new $paramModel();
$requestModelType = $requestModelInstance->getType();
$usedRequestModels[] = $requestModelType;
if ($node['x-global'] ?? false) {
$body['schema']['properties'][$name]['x-global'] = true;
$property = $array
? ['type' => 'array', 'description' => $node['description'], 'items' => ['$ref' => '#/definitions/' . $requestModelType]]
: ['$ref' => '#/definitions/' . $requestModelType];
} else {
$property = [
'type' => $node['type'],
'description' => $node['description'],
'default' => $node['default'] ?? null,
'x-example' => $node['x-example'] ?? null,
];
if (isset($node['enum'])) {
$property['enum'] = $node['enum'];
$property['x-enum-name'] = $node['x-enum-name'] ?? null;
$property['x-enum-keys'] = $node['x-enum-keys'] ?? null;
}
if ($node['x-global'] ?? false) {
$property['x-global'] = true;
}
if (\array_key_exists('items', $node)) {
$property['items'] = $node['items'];
}
}
if ($isNullable) {
$body['schema']['properties'][$name]['x-nullable'] = true;
}
if (\array_key_exists('items', $node)) {
$body['schema']['properties'][$name]['items'] = $node['items'];
$property['x-nullable'] = true;
}
}
@@ -750,11 +765,11 @@ class Swagger2 extends Format
$output['paths'][$url][\strtolower($route->getMethod())] = $temp;
}
foreach ($this->models as $model) {
foreach ($this->responseModels as $model) {
$this->getNestedModels($model, $usedModels);
}
foreach ($this->models as $model) {
foreach ($this->responseModels as $model) {
if (!in_array($model->getType(), $usedModels)) {
continue;
}
@@ -927,6 +942,89 @@ class Swagger2 extends Format
$output['definitions'][$model->getType()]['example'] = $examples;
}
// Generate request model definitions
foreach ($this->requestModels as $model) {
$this->getNestedRequestModels($model, $usedRequestModels);
}
foreach ($this->requestModels as $model) {
if (!in_array($model->getType(), $usedRequestModels)) {
continue;
}
$required = $model->getRequired();
$rules = $model->getRules();
$output['definitions'][$model->getType()] = [
'description' => $model->getName(),
'type' => 'object',
];
if (!empty($rules)) {
$output['definitions'][$model->getType()]['properties'] = [];
}
if (!empty($required)) {
$output['definitions'][$model->getType()]['required'] = $required;
}
foreach ($rules as $name => $rule) {
$ruleType = $rule['type'];
$isArray = $rule['array'] ?? false;
[$type, $format] = match ($ruleType) {
'string', 'datetime' => ['string', null],
'enum' => ['string', null],
'json' => ['object', null],
'array' => ['array', null],
'integer' => ['integer', 'int32'],
'float' => ['number', 'float'],
'double' => ['number', 'double'],
'boolean' => ['boolean', null],
'payload' => ['payload', null],
default => ['object', null],
};
$property = &$output['definitions'][$model->getType()]['properties'][$name];
if ($ruleType === 'json') {
$property = ['type' => $type, 'additionalProperties' => true, 'description' => $rule['description'] ?? '', 'x-example' => $rule['example'] ?? null];
continue;
}
$property = ['type' => $isArray ? 'array' : $type, 'description' => $rule['description'] ?? '', 'x-example' => $rule['example'] ?? null];
if ($isArray) {
$property['items'] = ['type' => $type];
if ($format) {
$property['items']['format'] = $format;
}
} elseif ($format) {
$property['format'] = $format;
}
// Handle nested model references
if ($type === 'object' && $ruleType && $ruleType !== 'json') {
$refKey = $isArray ? 'x-anyOf' : 'x-oneOf';
$property['items'] = \is_array($ruleType)
? [$refKey => \array_map(fn ($t) => ['$ref' => '#/definitions/' . $t], $ruleType)]
: ['type' => 'object', '$ref' => '#/definitions/' . $ruleType];
}
if ($ruleType === 'enum' && !empty($rule['enum'])) {
$target = $isArray ? $property['items'] : $property;
$target['enum'] = \array_values($rule['enum']);
$isArray ? $property['items'] = $target : $property = $target;
}
if (!in_array($name, $required)) {
$property['x-nullable'] = true;
}
}
$output['definitions'][$model->getType()]['example'] = \array_map(fn ($r) => $r['example'] ?? null, $rules);
}
\ksort($output['paths']);
return $output;
-10
View File
@@ -9,11 +9,8 @@ abstract class Model
public const TYPE_FLOAT = 'double';
public const TYPE_BOOLEAN = 'boolean';
public const TYPE_JSON = 'json';
public const TYPE_MODEL = 'json';
public const TYPE_DATETIME = 'datetime';
public const TYPE_DATETIME_EXAMPLE = '2020-10-15T06:38:00.000+00:00';
public const TYPE_RELATIONSHIP = 'relationship';
public const TYPE_PAYLOAD = 'payload';
public const TYPE_ARRAY = 'array';
public const TYPE_ENUM = 'enum';
@@ -22,11 +19,6 @@ abstract class Model
*/
protected array $rules = [];
/**
* @var array
*/
public array $conditions = [];
/**
* Get name
*
@@ -95,13 +87,11 @@ abstract class Model
public function getRequired(): array
{
$list = [];
foreach ($this->rules as $key => $rule) {
if ($rule['required'] ?? false) {
$list[] = $key;
}
}
return $list;
}
}