feat(specs): add discriminators for polymorphic responses

This commit is contained in:
Chirag Aggarwal
2026-04-16 11:07:07 +05:30
parent 6e50ed050d
commit 680cb04de7
11 changed files with 231 additions and 23 deletions
+4 -2
View File
@@ -117,7 +117,9 @@ use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -190,8 +192,8 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
Response::setModel(new ProviderRepositoryFrameworkList());
Response::setModel(new ProviderRepositoryRuntimeList());
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
@@ -307,6 +307,7 @@ class Create extends Action
];
}
$output->setAttribute('type', $type);
$output->setAttribute('variables', $variables);
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
@@ -313,6 +313,7 @@ class XList extends Action
}, $repos);
$response->dynamic(new Document([
'type' => $type,
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => $total,
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
+111
View File
@@ -263,6 +263,117 @@ abstract class Format
return $contents;
}
protected function getRegisteredModel(string $type): Model
{
foreach ($this->models as $model) {
if ($model->getType() === $type) {
return $model;
}
}
throw new \RuntimeException("Unresolved model '{$type}'. Ensure the model is registered.");
}
/**
* @param array<Model> $models
* @return array<string, mixed>|null
*/
protected function getUnionDiscriminator(array $models, string $refPrefix): ?array
{
if (\count($models) < 2) {
return null;
}
$candidateKeys = null;
foreach ($models as $model) {
$keys = [];
foreach ($model->conditions as $key => $condition) {
if ($this->isDiscriminatorConditionSupported($condition)) {
$keys[] = $key;
}
}
$candidateKeys = $candidateKeys === null
? $keys
: \array_values(\array_intersect($candidateKeys, $keys));
}
if (empty($candidateKeys)) {
return null;
}
foreach ($candidateKeys as $key) {
$mapping = [];
$matchedModels = [];
foreach ($models as $model) {
$rules = $model->getRules();
if (!isset($rules[$key]) || ($rules[$key]['required'] ?? false) !== true) {
continue 2;
}
$condition = $model->conditions[$key];
$values = \is_array($condition) ? $condition : [$condition];
if (isset($rules[$key]['enum']) && \is_array($rules[$key]['enum'])) {
$values = \array_values(\array_filter(
$values,
fn (mixed $value) => \in_array($value, $rules[$key]['enum'], true)
));
}
if ($values === []) {
continue 2;
}
foreach ($values as $value) {
$mappingKey = \is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
if (isset($mapping[$mappingKey]) && $mapping[$mappingKey] !== $refPrefix . $model->getType()) {
continue 2;
}
$mapping[$mappingKey] = $refPrefix . $model->getType();
}
$matchedModels[$model->getType()] = true;
}
if (\count($matchedModels) !== \count($models)) {
continue;
}
return [
'propertyName' => $key,
'mapping' => $mapping,
];
}
return null;
}
protected function isDiscriminatorConditionSupported(mixed $condition): bool
{
if (\is_scalar($condition) || \is_bool($condition)) {
return true;
}
if (!\is_array($condition) || $condition === []) {
return false;
}
foreach ($condition as $value) {
if (!(\is_scalar($value) || \is_bool($value))) {
return false;
}
}
return true;
}
protected function getRequestEnumName(string $service, string $method, string $param): ?string
{
/* `$service` is `$namespace` */
@@ -316,9 +316,10 @@ class OpenAPI3 extends Format
'description' => $modelDescription,
'content' => [
$produces => [
'schema' => [
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model)
],
'schema' => \array_filter([
'oneOf' => \array_map(fn ($m) => ['$ref' => '#/components/schemas/' . $m->getType()], $model),
'discriminator' => $this->getUnionDiscriminator($model, '#/components/schemas/'),
]),
],
],
];
@@ -901,17 +902,25 @@ class OpenAPI3 extends Format
if (\is_array($rule['type'])) {
if ($rule['array']) {
$items = [
$items = \array_filter([
'anyOf' => \array_map(function ($type) {
return ['$ref' => '#/components/schemas/' . $type];
}, $rule['type'])
];
}, $rule['type']),
'discriminator' => $this->getUnionDiscriminator(
\array_map(fn (string $type) => $this->getRegisteredModel($type), $rule['type']),
'#/components/schemas/'
),
]);
} else {
$items = [
$items = \array_filter([
'oneOf' => \array_map(function ($type) {
return ['$ref' => '#/components/schemas/' . $type];
}, $rule['type'])
];
}, $rule['type']),
'discriminator' => $this->getUnionDiscriminator(
\array_map(fn (string $type) => $this->getRegisteredModel($type), $rule['type']),
'#/components/schemas/'
),
]);
}
} else {
$items = [
@@ -322,11 +322,12 @@ class Swagger2 extends Format
}
$temp['responses'][(string)$response->getCode() ?? '500'] = [
'description' => $modelDescription,
'schema' => [
'schema' => \array_filter([
'x-oneOf' => \array_map(function ($m) {
return ['$ref' => '#/definitions/' . $m->getType()];
}, $model)
],
}, $model),
'x-discriminator' => $this->getUnionDiscriminator($model, '#/definitions/'),
]),
];
} else {
// Response definition using one type
@@ -881,13 +882,21 @@ class Swagger2 extends Format
if (\is_array($rule['type'])) {
if ($rule['array']) {
$items = [
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
];
$items = \array_filter([
'x-anyOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
'x-discriminator' => $this->getUnionDiscriminator(
\array_map(fn (string $type) => $this->getRegisteredModel($type), $rule['type']),
'#/definitions/'
),
]);
} else {
$items = [
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type'])
];
$items = \array_filter([
'x-oneOf' => \array_map(fn ($type) => ['$ref' => '#/definitions/' . $type], $rule['type']),
'x-discriminator' => $this->getUnionDiscriminator(
\array_map(fn (string $type) => $this->getRegisteredModel($type), $rule['type']),
'#/definitions/'
),
]);
}
} else {
$items = [
@@ -7,9 +7,16 @@ use Appwrite\Utopia\Response\Model;
abstract class Detection extends Model
{
public function __construct()
public function __construct(string $type)
{
$this
->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Repository detection type.',
'default' => $type,
'example' => $type,
'enum' => ['runtime', 'framework'],
])
->addRule('variables', [
'type' => Response::MODEL_DETECTION_VARIABLE,
'description' => 'Environment variables found in .env files',
@@ -8,7 +8,11 @@ class DetectionFramework extends Detection
{
public function __construct()
{
parent::__construct();
$this->conditions = [
'type' => 'framework',
];
parent::__construct('framework');
$this
->addRule('framework', [
@@ -8,7 +8,11 @@ class DetectionRuntime extends Detection
{
public function __construct()
{
parent::__construct();
$this->conditions = [
'type' => 'runtime',
];
parent::__construct('runtime');
$this
->addRule('runtime', [
@@ -0,0 +1,30 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ProviderRepositoryFrameworkList extends BaseList
{
public array $conditions = [
'type' => 'framework',
];
public function __construct()
{
parent::__construct(
'Framework Provider Repositories List',
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
'frameworkProviderRepositories',
Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK
);
$this->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Repository detection type.',
'default' => 'framework',
'example' => 'framework',
'enum' => ['runtime', 'framework'],
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ProviderRepositoryRuntimeList extends BaseList
{
public array $conditions = [
'type' => 'runtime',
];
public function __construct()
{
parent::__construct(
'Runtime Provider Repositories List',
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
'runtimeProviderRepositories',
Response::MODEL_PROVIDER_REPOSITORY_RUNTIME
);
$this->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Repository detection type.',
'default' => 'runtime',
'example' => 'runtime',
'enum' => ['runtime', 'framework'],
]);
}
}