Skip to content

Commit

Permalink
Merge 7e7d6e2 into f193281
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Mar 16, 2021
2 parents f193281 + 7e7d6e2 commit 00b6a5d
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 27 deletions.
6 changes: 2 additions & 4 deletions src/JsonSchema/SchemaFactory.php
Expand Up @@ -80,7 +80,7 @@ public function buildSchema(string $className, string $format = 'json', string $
}

$version = $schema->getVersion();
$definitionName = $this->buildDefinitionName($className, $format, $type, $operationType, $operationName, $serializerContext);
$definitionName = $this->buildDefinitionName($className, $format, $inputOrOutputClass, $resourceMetadata, $serializerContext);

if (null === $operationType || null === $operationName) {
$method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
Expand Down Expand Up @@ -239,10 +239,8 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
}

private function buildDefinitionName(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
private function buildDefinitionName(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?ResourceMetadata $resourceMetadata = null, ?array $serializerContext = null): string
{
[$resourceMetadata, $serializerContext,, $inputOrOutputClass] = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext);

$prefix = $resourceMetadata ? $resourceMetadata->getShortName() : (new \ReflectionClass($className))->getShortName();
if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) {
$parts = explode('\\', $inputOrOutputClass);
Expand Down
2 changes: 1 addition & 1 deletion src/JsonSchema/TypeFactory.php
Expand Up @@ -142,7 +142,7 @@ private function getClassType(?string $className, string $format, ?bool $readabl
throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.');
}

$subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext);
$subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext, false);

return ['$ref' => $subSchema['$ref']];
}
Expand Down
74 changes: 60 additions & 14 deletions src/OpenApi/Factory/OpenApiFactory.php
Expand Up @@ -86,19 +86,19 @@ public function __invoke(array $context = []): OpenApi
$servers = '/' === $baseUrl || '' === $baseUrl ? [new Model\Server('/')] : [new Model\Server($baseUrl)];
$paths = new Model\Paths();
$links = [];
$schemas = [];
$schemas = new \ArrayObject();

foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
$resourceShortName = $resourceMetadata->getShortName();

// Items needs to be parsed first to be able to reference the lines from the collection operation
[$itemOperationLinks, $itemOperationSchemas] = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::ITEM, $context, $paths, $links, $schemas);
$schemas += $itemOperationSchemas;
$this->appendSchemaDefinitions($schemas, $itemOperationSchemas);
[$collectionOperationLinks, $collectionOperationSchemas] = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::COLLECTION, $context, $paths, $links, $schemas);

[$subresourceOperationLinks, $subresourceOperationSchemas] = $this->collectPaths($resourceMetadata, $resourceClass, OperationType::SUBRESOURCE, $context, $paths, $links, $schemas);
$schemas += $collectionOperationSchemas;
$this->appendSchemaDefinitions($schemas, $collectionOperationSchemas);
}

$securitySchemes = $this->getSecuritySchemes();
Expand All @@ -113,7 +113,7 @@ public function __invoke(array $context = []): OpenApi
$servers,
$paths,
new Model\Components(
new \ArrayObject($schemas),
$schemas,
new \ArrayObject(),
new \ArrayObject(),
new \ArrayObject(),
Expand All @@ -128,7 +128,7 @@ public function __invoke(array $context = []): OpenApi
/**
* @return array | array
*/
private function collectPaths(ResourceMetadata $resourceMetadata, string $resourceClass, string $operationType, array $context, Model\Paths $paths, array &$links, array $schemas = []): array
private function collectPaths(ResourceMetadata $resourceMetadata, string $resourceClass, string $operationType, array $context, Model\Paths $paths, array &$links, \ArrayObject $schemas): array
{
$resourceShortName = $resourceMetadata->getShortName();
$operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : (OperationType::ITEM === $operationType ? $resourceMetadata->getItemOperations() : $this->subresourceOperationFactory->create($resourceClass));
Expand All @@ -155,12 +155,14 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
$linkedOperationId = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM);
$pathItem = $paths->getPath($path) ?: new Model\PathItem();
$forceSchemaCollection = OperationType::SUBRESOURCE === $operationType ? ($operation['collection'] ?? false) : false;
$schema = new Schema('openapi');
$schema->setDefinitions($schemas);

$operationOutputSchemas = [];
foreach ($responseMimeTypes as $operationFormat) {
$operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operationType, $operationName, new Schema('openapi'), null, $forceSchemaCollection);
$schemas += $operationOutputSchema->getDefinitions()->getArrayCopy();
$operationOutputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_OUTPUT, $operationType, $operationName, $schema, null, $forceSchemaCollection);
$operationOutputSchemas[$operationFormat] = $operationOutputSchema;
$this->appendSchemaDefinitions($schemas, $operationOutputSchema->getDefinitions());
}

$parameters = [];
Expand All @@ -175,19 +177,42 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
// Set up parameters
if (OperationType::ITEM === $operationType) {
foreach ($identifiers as $parameterName => $identifier) {
$parameters[] = new Model\Parameter(\is_string($parameterName) ? $parameterName : $identifier, 'path', 'Resource identifier', true, false, false, ['type' => 'string']);
$parameterName = \is_string($parameterName) ? $parameterName : $identifier;
$parameter = new Model\Parameter($parameterName, 'path', 'Resource identifier', true, false, false, ['type' => 'string']);
if ($this->hasParameter($parameter, $parameters)) {
continue;
}

$parameters[] = $parameter;
}
$links[$operationId] = $this->getLink($resourceClass, $operationId, $path);
} elseif (OperationType::COLLECTION === $operationType && 'GET' === $method) {
$parameters = array_merge($parameters, $this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($resourceMetadata, $operationName, $resourceClass));
foreach (array_merge($this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($resourceMetadata, $operationName, $resourceClass)) as $parameter) {
if ($this->hasParameter($parameter, $parameters)) {
continue;
}

$parameters[] = $parameter;
}
} elseif (OperationType::SUBRESOURCE === $operationType) {
foreach ($operation['identifiers'] as $parameterName => [$class, $property]) {
$parameters[] = new Model\Parameter($parameterName, 'path', $this->resourceMetadataFactory->create($class)->getShortName().' identifier', true, false, false, ['type' => 'string']);
$parameter = new Model\Parameter($parameterName, 'path', $this->resourceMetadataFactory->create($class)->getShortName().' identifier', true, false, false, ['type' => 'string']);
if ($this->hasParameter($parameter, $parameters)) {
continue;
}

$parameters[] = $parameter;
}

if ($operation['collection']) {
$subresourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
$parameters = array_merge($parameters, $this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($subresourceMetadata, $operationName, $resourceClass));
foreach (array_merge($this->getPaginationParameters($resourceMetadata, $operationName), $this->getFiltersParameters($subresourceMetadata, $operationName, $resourceClass)) as $parameter) {
if ($this->hasParameter($parameter, $parameters)) {
continue;
}

$parameters[] = $parameter;
}
}
}

Expand Down Expand Up @@ -241,9 +266,9 @@ private function collectPaths(ResourceMetadata $resourceMetadata, string $resour
} elseif ('PUT' === $method || 'POST' === $method || 'PATCH' === $method) {
$operationInputSchemas = [];
foreach ($requestMimeTypes as $operationFormat) {
$operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operationType, $operationName, new Schema('openapi'), null, $forceSchemaCollection);
$schemas += $operationInputSchema->getDefinitions()->getArrayCopy();
$operationInputSchema = $this->jsonSchemaFactory->buildSchema($resourceClass, $operationFormat, Schema::TYPE_INPUT, $operationType, $operationName, $schema, null, $forceSchemaCollection);
$operationInputSchemas[$operationFormat] = $operationInputSchema;
$this->appendSchemaDefinitions($schemas, $operationInputSchema->getDefinitions());
}

$requestBody = new Model\RequestBody(sprintf('The %s %s resource', 'POST' === $method ? 'new' : 'updated', $resourceShortName), $this->buildContent($requestMimeTypes, $operationInputSchemas), true);
Expand Down Expand Up @@ -396,7 +421,7 @@ private function getFiltersParameters(ResourceMetadata $resourceMetadata, string
$schema,
'array' === $schema['type'] && \in_array($data['type'],
[Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true) ? 'deepObject' : 'form',
'array' === $schema['type'],
$data['openapi']['explode'] ?? ('array' === $schema['type']),
$data['openapi']['allowReserved'] ?? false,
$data['openapi']['example'] ?? null,
isset($data['openapi']['examples']
Expand Down Expand Up @@ -486,4 +511,25 @@ private function getSecuritySchemes(): array

return $securitySchemes;
}

private function appendSchemaDefinitions(\ArrayObject &$schemas, \ArrayObject $definitions): void
{
foreach ($definitions as $key => $value) {
$schemas[$key] = $value;
}
}

/**
* @var Model\Parameter[]
*/
private function hasParameter(Model\Parameter $parameter, array $parameters): bool
{
foreach ($parameters as $existingParameter) {
if ($existingParameter->getName() === $parameter->getName() && $existingParameter->getIn() === $parameter->getIn()) {
return true;
}
}

return false;
}
}
10 changes: 10 additions & 0 deletions src/OpenApi/Model/Encoding.php
Expand Up @@ -52,11 +52,21 @@ public function canExplode(): bool
return $this->explode;
}

public function getExplode(): bool
{
return $this->explode;
}

public function canAllowReserved(): bool
{
return $this->allowReserved;
}

public function getAllowReserved(): bool
{
return $this->allowReserved;
}

public function withContentType(string $contentType): self
{
$clone = clone $this;
Expand Down
12 changes: 11 additions & 1 deletion src/OpenApi/Model/Parameter.php
Expand Up @@ -83,7 +83,7 @@ public function getDeprecated(): bool
return $this->deprecated;
}

public function canAllowEmptyValue(): bool
public function getAllowEmptyValue(): bool
{
return $this->allowEmptyValue;
}
Expand All @@ -103,11 +103,21 @@ public function canExplode(): bool
return $this->explode;
}

public function getExplode(): bool
{
return $this->explode;
}

public function canAllowReserved(): bool
{
return $this->allowReserved;
}

public function getAllowReserved(): bool
{
return $this->allowReserved;
}

public function getExample()
{
return $this->example;
Expand Down
2 changes: 1 addition & 1 deletion tests/JsonSchema/TypeFactoryTest.php
Expand Up @@ -372,7 +372,7 @@ public function testGetClassType(): void
{
$schemaFactoryProphecy = $this->prophesize(SchemaFactoryInterface::class);

$schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, Argument::type(Schema::class), ['foo' => 'bar'])->will(function (array $args) {
$schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, Argument::type(Schema::class), ['foo' => 'bar'], false)->will(function (array $args) {
$args[5]['$ref'] = 'ref';

return $args[5];
Expand Down
17 changes: 11 additions & 6 deletions tests/OpenApi/Factory/OpenApiFactoryTest.php
Expand Up @@ -77,7 +77,8 @@ public function testInvoke(): void
'custom' => ['method' => 'HEAD', 'path' => '/foo/{id}', 'openapi_context' => [
'description' => 'Custom description',
'parameters' => [
['description' => 'Test parameter', 'name' => 'param', 'in' => 'path', 'type' => 'string', 'required' => true, 'default' => 'BOTH'],
['description' => 'Test parameter', 'name' => 'param', 'in' => 'path', 'required' => true],
['description' => 'Replace parameter', 'name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'string', 'format' => 'uuid']],
],
'tags' => ['Dummy', 'Profile'],
'responses' => [
Expand Down Expand Up @@ -117,7 +118,11 @@ public function testInvoke(): void
'formats' => ['method' => 'PUT', 'path' => '/formatted/{id}', 'output_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']], 'input_formats' => ['json' => ['application/json'], 'csv' => ['text/csv']]],
],
[
'get' => ['method' => 'GET'] + self::OPERATION_FORMATS,
'get' => ['method' => 'GET', 'openapi_context' => [
'parameters' => [
['description' => 'Test modified collection page number', 'name' => 'page', 'in' => 'query', 'required' => false, 'schema' => ['type' => 'integer', 'default' => 1], 'allowEmptyValue' => true],
],
]] + self::OPERATION_FORMATS,
'post' => ['method' => 'POST'] + self::OPERATION_FORMATS,
'filtered' => ['method' => 'GET', 'filters' => ['f1', 'f2', 'f3', 'f4', 'f5'], 'path' => '/filtered'] + self::OPERATION_FORMATS,
'paginated' => ['method' => 'GET', 'pagination_client_enabled' => true, 'pagination_client_items_per_page' => true, 'pagination_items_per_page' => 20, 'pagination_maximum_items_per_page' => 80, 'path' => '/paginated'] + self::OPERATION_FORMATS,
Expand Down Expand Up @@ -153,7 +158,7 @@ public function testInvoke(): void
'type' => 'string',
'required' => true,
'strategy' => 'exact',
'openapi' => ['example' => 'bar', 'deprecated' => true, 'allowEmptyValue' => true, 'allowReserved' => true],
'openapi' => ['example' => 'bar', 'deprecated' => true, 'allowEmptyValue' => true, 'allowReserved' => true, 'explode' => true],
]]),
'f2' => new DummyFilter(['ha' => [
'property' => 'foo',
Expand Down Expand Up @@ -306,7 +311,7 @@ public function testInvoke(): void
'Retrieves the collection of Dummy resources.',
null,
[
new Model\Parameter('page', 'query', 'The collection page number', false, false, true, [
new Model\Parameter('page', 'query', 'Test modified collection page number', false, false, true, [
'type' => 'integer',
'default' => 1,
]),
Expand Down Expand Up @@ -434,7 +439,7 @@ public function testInvoke(): void
'Dummy',
'Custom description',
null,
[new Model\Parameter('param', 'path', 'Test parameter', true), new Model\Parameter('id', 'path', 'Resource identifier', true, false, false, ['type' => 'string'])],
[new Model\Parameter('param', 'path', 'Test parameter', true), new Model\Parameter('id', 'path', 'Replace parameter', true, false, false, ['type' => 'string', 'format' => 'uuid'])],
new Model\RequestBody('Custom request body', new \ArrayObject([
'multipart/form-data' => [
'schema' => [
Expand Down Expand Up @@ -512,7 +517,7 @@ public function testInvoke(): void
]),
new Model\Parameter('name', 'query', '', true, true, true, [
'type' => 'string',
], 'form', false, true, 'bar'),
], 'form', true, true, 'bar'),
new Model\Parameter('ha', 'query', '', false, false, true, [
'type' => 'integer',
]),
Expand Down

0 comments on commit 00b6a5d

Please sign in to comment.