Skip to content

Commit

Permalink
Merge pull request #3309 from teohhanhui/fix/non-resource-type-json-s…
Browse files Browse the repository at this point in the history
…chema

Fix JSON Schema generation for non-resource class
  • Loading branch information
teohhanhui committed Dec 23, 2019
2 parents f78e3e3 + 3fb0f25 commit 778c0c1
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 39 deletions.
2 changes: 2 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/json_schema.xml
Expand Up @@ -8,6 +8,7 @@
<defaults public="false" />

<service id="api_platform.json_schema.type_factory" class="ApiPlatform\Core\JsonSchema\TypeFactory">
<argument type="service" id="api_platform.resource_class_resolver" />
<call method="setSchemaFactory">
<argument type="service" id="api_platform.json_schema.schema_factory"/>
</call>
Expand All @@ -20,6 +21,7 @@
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
<argument type="service" id="api_platform.resource_class_resolver" />
</service>
<service id="ApiPlatform\Core\JsonSchema\SchemaFactoryInterface" alias="api_platform.json_schema.schema_factory" />

Expand Down
6 changes: 3 additions & 3 deletions src/Hydra/JsonSchema/SchemaFactory.php
Expand Up @@ -18,7 +18,7 @@
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;

/**
* Generates the JSON Schema corresponding to a Hydra document.
* Decorator factory which adds Hydra properties to the JSON Schema document.
*
* @experimental
*
Expand Down Expand Up @@ -47,9 +47,9 @@ public function __construct(BaseSchemaFactory $schemaFactory)
/**
* {@inheritdoc}
*/
public function buildSchema(string $resourceClass, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
public function buildSchema(string $className, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
{
$schema = $this->schemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
$schema = $this->schemaFactory->buildSchema($className, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
if ('jsonld' !== $format) {
return $schema;
}
Expand Down
51 changes: 34 additions & 17 deletions src/JsonSchema/SchemaFactory.php
Expand Up @@ -14,12 +14,14 @@
namespace ApiPlatform\Core\JsonSchema;

use ApiPlatform\Core\Api\OperationType;
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
Expand All @@ -33,20 +35,22 @@
*/
final class SchemaFactory implements SchemaFactoryInterface
{
private $resourceMetadataFactory;
use ResourceClassInfoTrait;

private $typeFactory;
private $propertyNameCollectionFactory;
private $propertyMetadataFactory;
private $typeFactory;
private $nameConverter;
private $distinctFormats = [];

public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null)
public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null)
{
$this->typeFactory = $typeFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->nameConverter = $nameConverter;
$this->typeFactory = $typeFactory;
$this->resourceClassResolver = $resourceClassResolver;
}

/**
Expand All @@ -62,16 +66,20 @@ public function addDistinctFormat(string $format): void
/**
* {@inheritdoc}
*/
public function buildSchema(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
{
$schema = $schema ?? new Schema();
if (null === $metadata = $this->getMetadata($resourceClass, $type, $operationType, $operationName, $serializerContext)) {
if (null === $metadata = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext)) {
return $schema;
}
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata;

if (null === $resourceMetadata && (null !== $operationType || null !== $operationName)) {
throw new \LogicException('The $operationType and $operationName arguments must be null for non-resource class.');
}

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

if (null === $operationType || null === $operationName) {
$method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
Expand Down Expand Up @@ -103,12 +111,13 @@ public function buildSchema(string $resourceClass, string $format = 'json', stri

$definition = new \ArrayObject(['type' => 'object']);
$definitions[$definitionName] = $definition;
if (null !== $description = $resourceMetadata->getDescription()) {
if (null !== $resourceMetadata && null !== $description = $resourceMetadata->getDescription()) {
$definition['description'] = $description;
}
// see https://github.com/json-schema-org/json-schema-spec/pull/737
if (
Schema::VERSION_SWAGGER !== $version &&
null !== $resourceMetadata &&
(
(null !== $operationType && null !== $operationName && null !== $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) ||
null !== $resourceMetadata->getAttribute('deprecation_reason', null)
Expand All @@ -118,7 +127,7 @@ public function buildSchema(string $resourceClass, string $format = 'json', stri
}
// externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
// See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
if (null !== $iri = $resourceMetadata->getIri()) {
if (null !== $resourceMetadata && null !== $iri = $resourceMetadata->getIri()) {
$definition['externalDocs'] = ['url' => $iri];
}

Expand Down Expand Up @@ -200,12 +209,12 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
}

private function buildDefinitionName(string $resourceClass, 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 $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
{
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($resourceClass, $type, $operationType, $operationName, $serializerContext);
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext);

$prefix = $resourceMetadata->getShortName();
if (null !== $inputOrOutputClass && $resourceClass !== $inputOrOutputClass) {
$prefix = $resourceMetadata ? $resourceMetadata->getShortName() : (new \ReflectionClass($className))->getShortName();
if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) {
$prefix .= ':'.md5($inputOrOutputClass);
}

Expand All @@ -224,14 +233,22 @@ private function buildDefinitionName(string $resourceClass, string $format = 'js
return $name;
}

private function getMetadata(string $resourceClass, string $type = Schema::TYPE_OUTPUT, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
private function getMetadata(string $className, string $type = Schema::TYPE_OUTPUT, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
{
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
if (!$this->isResourceClass($className)) {
return [
null,
$serializerContext ?? [],
$className,
];
}

$resourceMetadata = $this->resourceMetadataFactory->create($className);
$attribute = Schema::TYPE_OUTPUT === $type ? 'output' : 'input';
if (null === $operationType || null === $operationName) {
$inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $resourceClass]);
$inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $className]);
} else {
$inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $resourceClass], true);
$inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $className], true);
}

if (null === ($inputOrOutput['class'] ?? null)) {
Expand Down
8 changes: 3 additions & 5 deletions src/JsonSchema/SchemaFactoryInterface.php
Expand Up @@ -13,10 +13,8 @@

namespace ApiPlatform\Core\JsonSchema;

use ApiPlatform\Core\Exception\ResourceClassNotFoundException;

/**
* Builds a JSON Schema from an API Platform resource definition.
* Factory for creating the JSON Schema document corresponding to a PHP class.
*
* @experimental
*
Expand All @@ -25,7 +23,7 @@
interface SchemaFactoryInterface
{
/**
* @throws ResourceClassNotFoundException
* Builds the JSON Schema document corresponding to the given PHP class.
*/
public function buildSchema(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema;
public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema;
}
54 changes: 41 additions & 13 deletions src/JsonSchema/TypeFactory.php
Expand Up @@ -13,6 +13,9 @@

namespace ApiPlatform\Core\JsonSchema;

use ApiPlatform\Core\Api\ResourceClassResolverInterface;
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\PropertyInfo\Type;

/**
Expand All @@ -24,21 +27,25 @@
*/
final class TypeFactory implements TypeFactoryInterface
{
use ResourceClassInfoTrait;

/**
* @var SchemaFactoryInterface|null
*/
private $schemaFactory;

/**
* Injects the JSON Schema factory to use.
*/
public function __construct(ResourceClassResolverInterface $resourceClassResolver = null)
{
$this->resourceClassResolver = $resourceClassResolver;
}

public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
{
$this->schemaFactory = $schemaFactory;
}

/**
* Gets the OpenAPI type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided.
* {@inheritdoc}
*/
public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array
{
Expand Down Expand Up @@ -66,7 +73,7 @@ public function getType(Type $type, string $format = 'json', ?bool $readableLink
}

/**
* Gets the OpenAPI type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
*/
private function getClassType(?string $className, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array
{
Expand All @@ -75,20 +82,41 @@ private function getClassType(?string $className, string $format = 'json', ?bool
}

if (is_a($className, \DateTimeInterface::class, true)) {
return ['type' => 'string', 'format' => 'date-time'];
return [
'type' => 'string',
'format' => 'date-time',
];
}
if (is_a($className, UuidInterface::class, true)) {
return [
'type' => 'string',
'format' => 'uuid',
];
}

if (null !== $this->schemaFactory && true === $readableLink && null !== $schema) { // Skip if $baseSchema is null (filters only support basic types)
$version = $schema->getVersion();
// Skip if $schema is null (filters only support basic types)
if (null === $schema) {
return ['type' => 'string'];
}

$subSchema = new Schema($version);
$subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema
if ($this->isResourceClass($className) && true !== $readableLink) {
return [
'type' => 'string',
'format' => 'iri-reference',
];
}

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

return ['$ref' => $subSchema['$ref']];
$subSchema = new Schema($version);
$subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema

if (null === $this->schemaFactory) {
throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.');
}

return ['type' => 'string'];
$subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext);

return ['$ref' => $subSchema['$ref']];
}
}
7 changes: 6 additions & 1 deletion src/JsonSchema/TypeFactoryInterface.php
Expand Up @@ -16,11 +16,16 @@
use Symfony\Component\PropertyInfo\Type;

/**
* Gets the OpenAPI type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided.
* Factory for creating the JSON Schema document which specifies the data type corresponding to a PHP type.
*
* @experimental
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface TypeFactoryInterface
{
/**
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided.
*/
public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array;
}

0 comments on commit 778c0c1

Please sign in to comment.