diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3427963543..83dc36e11ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,6 +212,7 @@ jobs: composer global config allow-plugins.soyuka/pmu true --no-interaction composer global link . composer require --dev doctrine/mongodb-odm-bundle + composer require --dev --no-interaction "symfony/json-schema:8.2.x-dev" - name: Cache PHPStan results uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: @@ -990,6 +991,57 @@ jobs: - name: Run PHPUnit tests run: vendor/bin/phpunit + phpunit-json-schema-generator: + name: PHPUnit (PHP ${{ matrix.php }}) (Symfony JsonSchema generator) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + php: + - '8.5' + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php }} + tools: pecl, composer:2.9.8 + extensions: intl, bcmath, curl, openssl, mbstring + coverage: none + ini-values: memory_limit=-1 + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-json-schema-generator-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-json-schema-generator- + - name: Update project dependencies + run: | + composer global require "soyuka/pmu:$PMU_VERSION" + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . + composer require --dev --no-interaction "symfony/json-schema:8.2.x-dev" + - name: Remove cache + run: rm -Rf tests/Fixtures/app/var/cache/* + - name: Run golden schema tests (generator disabled) + run: vendor/bin/phpunit tests/Functional/JsonSchema/GoldenSchemaTest.php + - name: Clear test app cache + run: rm -Rf tests/Fixtures/app/var/cache/test + - name: Run JSON Schema tests (generator enabled) + env: + JSON_SCHEMA_GENERATOR: '1' + run: | + vendor/bin/phpunit tests/Functional/JsonSchema/JsonSchemaTest.php + vendor/bin/phpunit tests/Functional/JsonSchema/JsonLdJsonSchemaTest.php + vendor/bin/phpunit tests/Functional/OpenApiTest.php + - name: Run container extension tests + run: vendor/bin/phpunit src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php --filter JsonSchema + phpunit-symfony-lowest: name: PHPUnit (PHP ${{ matrix.php }}) (Symfony lowest) runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index 5be44f7275e..3fdf764fda6 100644 --- a/composer.json +++ b/composer.json @@ -211,6 +211,7 @@ "symfony/uid": "To support Symfony UUID/ULID identifiers.", "symfony/messenger": "To support messenger integration.", "symfony/web-profiler-bundle": "To use the data collector.", + "symfony/json-schema": "To generate JSON Schemas with the Symfony JsonSchema component (opt-in, api_platform.json_schema.generator).", "symfony/json-streamer": "To use the JSON Streamer component.", "webonyx/graphql-php": "To support GraphQL." }, diff --git a/src/Hydra/JsonSchema/ItemDefinitionProcessor.php b/src/Hydra/JsonSchema/ItemDefinitionProcessor.php new file mode 100644 index 00000000000..79dfe6eb46b --- /dev/null +++ b/src/Hydra/JsonSchema/ItemDefinitionProcessor.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\JsonSchema; + +use ApiPlatform\JsonSchema\Generator\ApiPlatformDefinitionPolicy; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\DefinitionParent; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; +use Symfony\Component\JsonSchema\SchemaDocument; + +final class ItemDefinitionProcessor implements DefinitionProcessorInterface +{ + use ResourceClassInfoTrait; + + public function __construct( + ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, + private readonly ApiPlatformDefinitionPolicy $definitionPolicy, + ) { + $this->resourceMetadataFactory = $resourceMetadataFactory; + } + + public function process(array $definition, string $class, Configuration $config, SchemaDocument $document, ?DefinitionParent $parent): array + { + if (null === $parent || 'jsonld' !== $config->format || !$this->isResourceClass($class)) { + return $definition; + } + + $baseName = $this->definitionPolicy->resolveGenId($parent) ? SchemaFactory::ITEM_BASE_SCHEMA_NAME : SchemaFactory::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME; + $item = ['allOf' => [['$ref' => $config->dialect->refPath.$baseName], $definition]]; + + if (isset($definition['description'])) { + $item['description'] = $definition['description']; + unset($item['allOf'][1]['description']); + } + + return $item; + } +} diff --git a/src/Hydra/JsonSchema/SchemaFactory.php b/src/Hydra/JsonSchema/SchemaFactory.php index aa715cc5ac5..0f83e807263 100644 --- a/src/Hydra/JsonSchema/SchemaFactory.php +++ b/src/Hydra/JsonSchema/SchemaFactory.php @@ -36,8 +36,8 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI use ResourceMetadataTrait; use SchemaUriPrefixTrait; - private const ITEM_BASE_SCHEMA_NAME = 'HydraItemBaseSchema'; - private const ITEM_WITHOUT_ID_BASE_SCHEMA_NAME = 'HydraItemBaseSchemaWithoutId'; + public const ITEM_BASE_SCHEMA_NAME = 'HydraItemBaseSchema'; + public const ITEM_WITHOUT_ID_BASE_SCHEMA_NAME = 'HydraItemBaseSchemaWithoutId'; private const COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION = 'HydraCollectionBaseSchemaNoPagination'; private const COLLECTION_BASE_SCHEMA_NAME = 'HydraCollectionBaseSchema'; @@ -130,6 +130,7 @@ public function buildSchema(string $className, string $format = 'jsonld', string $definitions = $schema->getDefinitions(); $prefix = $this->getSchemaUriPrefix($schema->getVersion()); $collectionKey = $schema->getItemsDefinitionKey(); + $this->addReferencedItemBaseSchemas($definitions, $prefix); if (!$collectionKey) { $definitionName = $schema->getRootDefinitionKey() ?? $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext); @@ -232,9 +233,8 @@ public function buildSchema(string $className, string $format = 'jsonld', string ]; } - $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext + ['schema_type' => $type]); $schema['type'] = 'object'; - $schema['description'] = "$definitionName collection."; + $schema['description'] = "$collectionKey collection."; $schema['allOf'] = [ ['$ref' => $prefix.(false === $operation->getPaginationEnabled() ? self::COLLECTION_BASE_SCHEMA_NAME_NO_PAGINATION : self::COLLECTION_BASE_SCHEMA_NAME)], [ @@ -267,6 +267,20 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void } } + private function addReferencedItemBaseSchemas(\ArrayObject $definitions, string $prefix): void + { + $references = []; + foreach ($definitions as $definition) { + $references[$definition['allOf'][0]['$ref'] ?? ''] = true; + } + + foreach ([self::ITEM_BASE_SCHEMA_NAME => self::ITEM_BASE_SCHEMA_WITH_ID, self::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME => self::ITEM_BASE_SCHEMA_WITHOUT_ID] as $name => $baseSchema) { + if (isset($references[$prefix.$name]) && !isset($definitions[$name])) { + $definitions[$name] = $baseSchema; + } + } + } + private function decorateItemDefinition(string $definitionName, \ArrayObject $definitions, string $prefix, string $type, ?array $serializerContext): void { if (!isset($definitions[$definitionName])) { diff --git a/src/JsonApi/JsonSchema/SchemaFactory.php b/src/JsonApi/JsonSchema/SchemaFactory.php index 87cb12d896a..d4b0ab154fb 100644 --- a/src/JsonApi/JsonSchema/SchemaFactory.php +++ b/src/JsonApi/JsonSchema/SchemaFactory.php @@ -123,7 +123,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes $jsonApiSerializerContext = $serializerContext; if (true === ($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) && $inputOrOutputClass === $className) { - unset($jsonApiSerializerContext['groups']); + $jsonApiSerializerContext['groups'] = null; } $schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection); diff --git a/src/JsonSchema/Generator/ApiPlatformDefinitionPolicy.php b/src/JsonSchema/Generator/ApiPlatformDefinitionPolicy.php new file mode 100644 index 00000000000..12d0471e4d5 --- /dev/null +++ b/src/JsonSchema/Generator/ApiPlatformDefinitionPolicy.php @@ -0,0 +1,258 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\JsonSchema\ResourceMetadataTrait; +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\Exception\PropertyNotFoundException; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\DefinitionParent; +use Symfony\Component\JsonSchema\DefinitionPolicy\DefinitionPolicyInterface; +use Symfony\Component\JsonSchema\SchemaDocument; + +final class ApiPlatformDefinitionPolicy implements DefinitionPolicyInterface +{ + use ResourceMetadataTrait; + + private const GLUE = '.'; + private const JSON_MERGE_PATCH_FORMAT = 'merge-patch+json'; + private const JSON_MERGE_PATCH_SCHEMA_POSTFIX = 'jsonMergePatch'; + + /** + * @var array + */ + private array $prefixCache = []; + + public function __construct( + ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, + ?ResourceClassResolverInterface $resourceClassResolver, + private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, + ) { + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->resourceClassResolver = $resourceClassResolver; + } + + public function isJsonMergePatch(?Operation $operation, string $type, string $format): bool + { + return 'json' === $format && 'PATCH' === $this->resolveMethod($operation, $type) && Schema::TYPE_INPUT === $type; + } + + public function isPartialUpdate(?Operation $operation, string $type, string $format): bool + { + $isNonStandardPut = Schema::TYPE_INPUT === $type && 'PUT' === $this->resolveMethod($operation, $type) && !($operation?->getExtraProperties()['standard_put'] ?? true); + + return $this->isJsonMergePatch($operation, $type, $format) || $isNonStandardPut; + } + + public function isPartial(string $class, Configuration $config, ?DefinitionParent $parent): bool + { + if (null === $parent) { + return $config->context[ConfigurationFactory::CONTEXT_PARTIAL] ?? false; + } + + $operation = $this->findNestedOperation($class, $config); + + return null !== $operation && $this->isPartialUpdate($operation, $config->context[ConfigurationFactory::CONTEXT_SCHEMA_TYPE] ?? Schema::TYPE_OUTPUT, $config->context[ConfigurationFactory::CONTEXT_FORMAT] ?? 'json'); + } + + public function createRootPrefix(?Operation $operation, string $className, string $inputOrOutputClass): string + { + $prefix = $operation ? $this->createPrefixFromOperation($operation) : null; + $prefix ??= $this->createPrefixFromClass($className); + + if ($className !== $inputOrOutputClass) { + $prefix .= self::GLUE.$this->createPrefixFromClass($inputOrOutputClass); + } + + return $prefix; + } + + public function nameFor(string $class, Configuration $config, SchemaDocument $document, ?DefinitionParent $parent = null): string + { + $isPartialUpdate = $this->isPartial($class, $config, $parent); + + if (null === $parent) { + $prefix = $config->definitionPrefix ?? $this->createPrefixFromClass($class); + $isJsonMergePatch = self::JSON_MERGE_PATCH_FORMAT === $config->format; + } else { + $operation = $this->findNestedOperation($class, $config); + $prefix = $this->createNestedPrefix($class, $operation); + $type = $config->context[ConfigurationFactory::CONTEXT_SCHEMA_TYPE] ?? Schema::TYPE_OUTPUT; + $format = $config->context[ConfigurationFactory::CONTEXT_FORMAT] ?? 'json'; + $isJsonMergePatch = null !== $operation && $this->isJsonMergePatch($operation, $type, $format); + } + + $format = $config->format ?? 'json'; + if (!\in_array($format, ['json', self::JSON_MERGE_PATCH_FORMAT], true)) { + $prefix .= self::GLUE.$format; + } + + if (null !== $config->definitionName) { + $name = \sprintf('%s%s', $prefix, '' !== $config->definitionName ? '-'.$config->definitionName : ''); + } else { + $parts = []; + + if ($config->groups) { + $parts[] = implode('_', $this->orderGroups($config->groups, $config)); + } + + if ($config->attributes) { + $parts[] = $this->getAttributesAsString($config->attributes); + } + + if (null !== $validationGroups = $config->getValidationGroupNames()) { + $parts[] = 'validation'.self::GLUE.($validationGroups ? implode('_', $validationGroups) : 'none'); + } + + $name = $parts ? \sprintf('%s-%s', $prefix, implode('_', $parts)) : $prefix; + } + + if ($isPartialUpdate && !$isJsonMergePatch) { + $name .= self::GLUE.'partial'; + } + + if (false === $this->resolveGenId($parent)) { + $name .= '_noid'; + } + + if ($isJsonMergePatch) { + $name .= self::GLUE.self::JSON_MERGE_PATCH_SCHEMA_POSTFIX; + } + + return preg_replace('/[^a-zA-Z0-9.\-_]/', self::GLUE, $name); + } + + public function resolveGenId(?DefinitionParent $parent): bool + { + for (; null !== $parent; $parent = $parent->parent) { + try { + $genId = $this->propertyMetadataFactory->create($parent->class, $parent->property)->getGenId(); + } catch (PropertyNotFoundException) { + continue; + } + + if (null !== $genId) { + return $genId; + } + } + + return true; + } + + public function findNestedOperation(string $class, Configuration $config): ?Operation + { + if (!$this->isResourceClass($class)) { + return null; + } + + return $this->findOperation($class, $config->context[ConfigurationFactory::CONTEXT_SCHEMA_TYPE] ?? Schema::TYPE_OUTPUT, null, [SchemaFactoryInterface::FORCE_SUBSCHEMA => true]); + } + + private function createNestedPrefix(string $class, ?Operation $operation): string + { + if (null === $operation) { + return $this->createPrefixFromClass($class); + } + + return $this->createPrefixFromOperation($operation) ?? $this->createPrefixFromClass($class); + } + + private function resolveMethod(?Operation $operation, string $type): string + { + if ($operation instanceof HttpOperation) { + return $operation->getMethod(); + } + + return !$operation && Schema::TYPE_INPUT === $type ? 'POST' : 'GET'; + } + + private function createPrefixFromOperation(Operation $operation): ?string + { + $name = $operation->getShortName(); + + if (null === $name) { + return null; + } + + if (!isset($this->prefixCache[$name])) { + $this->prefixCache[$name] = $operation->getClass(); + + return $name; + } + + if ($this->prefixCache[$name] === $operation->getClass()) { + return $name; + } + + return null; + } + + private function createPrefixFromClass(string $fullyQualifiedClassName, int $namespaceParts = 1): string + { + $parts = explode('\\', $fullyQualifiedClassName); + $name = implode(self::GLUE, \array_slice($parts, -$namespaceParts)); + + if (!isset($this->prefixCache[$name])) { + $this->prefixCache[$name] = $fullyQualifiedClassName; + + return $name; + } + + if ($this->prefixCache[$name] !== $fullyQualifiedClassName) { + $name = $this->createPrefixFromClass($fullyQualifiedClassName, ++$namespaceParts); + } + + return $name; + } + + /** + * @param array $attributes + */ + private function getAttributesAsString(array $attributes): string + { + $parts = []; + + foreach ($attributes as $key => $value) { + if (\is_array($value)) { + foreach (explode('_', $this->getAttributesAsString($value)) as $child) { + $parts[] = $key.self::GLUE.$child; + } + } elseif (\is_string($key)) { + $parts[] = $key; + } else { + $parts[] = $value; + } + } + + return implode('_', $parts); + } + + /** + * @param list $groups + * + * @return list + */ + private function orderGroups(array $groups, Configuration $config): array + { + $declared = $config->context[ConfigurationFactory::CONTEXT_GROUPS] ?? []; + + return array_values([...array_intersect($declared, $groups), ...array_diff($groups, $declared)]); + } +} diff --git a/src/JsonSchema/Generator/ApiPlatformPropertyInfoExtractor.php b/src/JsonSchema/Generator/ApiPlatformPropertyInfoExtractor.php new file mode 100644 index 00000000000..7203fae18ba --- /dev/null +++ b/src/JsonSchema/Generator/ApiPlatformPropertyInfoExtractor.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\Metadata\ApiProperty; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; + +final class ApiPlatformPropertyInfoExtractor implements PropertyInfoExtractorInterface, PropertyInitializableExtractorInterface +{ + public function __construct( + private readonly PropertyMetadataAccessor $accessor, + ) { + } + + public function getProperties(string $class, array $context = []): array + { + return $this->accessor->getNames($class, $context); + } + + public function getType(string $class, string $property, array $context = []): ?Type + { + $metadata = $this->accessor->get($class, $property, $context); + + if (null === $type = $metadata->getNativeType()) { + return null; + } + + if ($type instanceof CollectionType && null !== $metadata->getUriTemplate()) { + $type = $type->getCollectionValueType(); + } + + return $type; + } + + public function isReadable(string $class, string $property, array $context = []): bool + { + return $this->isExposed($this->accessor->get($class, $property, $context)); + } + + public function isWritable(string $class, string $property, array $context = []): bool + { + return $this->isExposed($this->accessor->get($class, $property, $context)); + } + + public function isInitializable(string $class, string $property, array $context = []): bool + { + return $this->isExposed($this->accessor->get($class, $property, $context)); + } + + public function getShortDescription(string $class, string $property, array $context = []): ?string + { + return null; + } + + public function getLongDescription(string $class, string $property, array $context = []): ?string + { + return null; + } + + private function isExposed(ApiProperty $metadata): bool + { + return false !== $metadata->isReadable() || false !== $metadata->isWritable(); + } +} diff --git a/src/JsonSchema/Generator/ApiPropertySchemaEnricher.php b/src/JsonSchema/Generator/ApiPropertySchemaEnricher.php new file mode 100644 index 00000000000..dec13eb898f --- /dev/null +++ b/src/JsonSchema/Generator/ApiPropertySchemaEnricher.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\PropertySchemaEnricher\PropertySchema; +use Symfony\Component\JsonSchema\PropertySchemaEnricher\PropertySchemaEnricherInterface; +use Symfony\Component\JsonSchema\SchemaDocument; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\ObjectType; + +final class ApiPropertySchemaEnricher implements PropertySchemaEnricherInterface +{ + private const STRUCTURAL_KEYWORDS = ['type', 'items', 'additionalProperties', 'anyOf', 'allOf', 'oneOf', '$ref', 'enum', 'format', 'const']; + private const OWNED_KEYWORDS = ['type', 'items', 'additionalProperties', 'anyOf', 'allOf', 'oneOf', '$ref', 'readOnly', 'writeOnly', 'description', 'deprecated', 'externalDocs', 'default', 'example']; + + public function __construct( + private readonly PropertyMetadataAccessor $accessor, + private readonly ResourceClassResolverInterface $resourceClassResolver, + ) { + } + + public function enrich(PropertySchema $property, Configuration $config, SchemaDocument $document): PropertySchema + { + $metadata = $this->accessor->get($property->class, $property->property, PropertyMetadataAccessor::contextFromConfiguration($config)); + $required = true === $metadata->isRequired(); + + if (true === ($metadata->getExtraProperties()[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) { + return $property->withRequired($required); + } + + $contextSchema = Schema::VERSION_JSON_SCHEMA === $config->context[ConfigurationFactory::CONTEXT_VERSION] ? $metadata->getJsonSchemaContext() : $metadata->getOpenapiContext(); + $metadataSchema = $metadata->getSchema() ?? []; + $schema = [...array_diff_key($property->schema, ['default' => true]), ...array_diff_key($metadataSchema, array_flip(self::OWNED_KEYWORDS)), ...$this->getDocumentation($metadata)]; + $schema = array_replace(array_intersect_key($metadataSchema, $schema), $schema); + + if ($contextSchema) { + if (array_intersect_key($contextSchema, array_flip(self::STRUCTURAL_KEYWORDS))) { + $schema = array_diff_key($schema, array_flip(self::STRUCTURAL_KEYWORDS)); + } + $schema = [...$schema, ...$contextSchema]; + } + + return $property->withSchema($schema)->withRequired($required); + } + + /** + * @return array + */ + private function getDocumentation(ApiProperty $metadata): array + { + $documentation = []; + + if (null !== $metadata->getUriTemplate() || (false === $metadata->isWritable() && !$metadata->isInitializable())) { + $documentation['readOnly'] = true; + } + + if (false === $metadata->isReadable()) { + $documentation['writeOnly'] = true; + } + + if (null !== $description = $metadata->getDescription()) { + $documentation['description'] = $description; + } + + if (null !== $metadata->getDeprecationReason()) { + $documentation['deprecated'] = true; + } + + if (null !== $iri = $metadata->getTypes()[0] ?? null) { + $documentation['externalDocs'] = ['url' => $iri]; + } + + $isResourceClass = $metadata->getNativeType()?->isSatisfiedBy(fn (Type $type): bool => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($type->getClassName())); + if (null !== ($default = $metadata->getDefault()) && [] !== $default && !$isResourceClass) { + $documentation['default'] = $default instanceof \BackedEnum ? $default->value : $default; + } + + if (null !== ($example = $metadata->getExample()) && [] !== $example) { + $documentation['example'] = $example; + } + + return $documentation; + } +} diff --git a/src/JsonSchema/Generator/ApiPropertySchemaProvider.php b/src/JsonSchema/Generator/ApiPropertySchemaProvider.php new file mode 100644 index 00000000000..2dcf20ca33c --- /dev/null +++ b/src/JsonSchema/Generator/ApiPropertySchemaProvider.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; +use ApiPlatform\JsonSchema\Schema; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\PropertySchemaProvider\PropertySchemaProviderInterface; +use Symfony\Component\JsonSchema\SchemaDocument; + +final class ApiPropertySchemaProvider implements PropertySchemaProviderInterface +{ + public function __construct( + private readonly PropertyMetadataAccessor $accessor, + ) { + } + + public function provide(string $class, string $property, Configuration $config, SchemaDocument $document): ?array + { + $metadata = $this->accessor->get($class, $property, PropertyMetadataAccessor::contextFromConfiguration($config)); + + if (true !== ($metadata->getExtraProperties()[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) { + return null; + } + + $contextSchema = Schema::VERSION_JSON_SCHEMA === $config->context[ConfigurationFactory::CONTEXT_VERSION] ? $metadata->getJsonSchemaContext() : $metadata->getOpenapiContext(); + + return array_merge($metadata->getSchema() ?? [], $contextSchema ?? []); + } +} diff --git a/src/JsonSchema/Generator/ConfigurationFactory.php b/src/JsonSchema/Generator/ConfigurationFactory.php new file mode 100644 index 00000000000..f82c8a5d8ea --- /dev/null +++ b/src/JsonSchema/Generator/ConfigurationFactory.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\JsonSchema\ResourceMetadataTrait; +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\Dialect; +use Symfony\Component\JsonSchema\NullSyntax; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\Validator\Constraints\GroupSequence; + +final class ConfigurationFactory +{ + use ResourceMetadataTrait; + + public const CONTEXT_SCHEMA_TYPE = 'api_platform.schema_type'; + public const CONTEXT_FORMAT = 'api_platform.format'; + public const CONTEXT_PARTIAL = 'api_platform.partial'; + public const CONTEXT_VERSION = 'api_platform.version'; + public const CONTEXT_PROPERTY_OPTIONS = 'api_platform.property_options'; + public const CONTEXT_GROUPS = 'api_platform.groups'; + public const CONTEXT_DESCRIPTION = 'api_platform.description'; + public const CONTEXT_DEPRECATED = 'api_platform.deprecated'; + public const CONTEXT_EXTERNAL_DOCS = 'api_platform.external_docs'; + + public const CONTEXT_KEYS = [ + self::CONTEXT_SCHEMA_TYPE, + self::CONTEXT_FORMAT, + self::CONTEXT_PARTIAL, + self::CONTEXT_VERSION, + self::CONTEXT_PROPERTY_OPTIONS, + self::CONTEXT_GROUPS, + self::CONTEXT_DESCRIPTION, + self::CONTEXT_DEPRECATED, + self::CONTEXT_EXTERNAL_DOCS, + ]; + + private const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; + + public function __construct( + ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, + ?ResourceClassResolverInterface $resourceClassResolver, + private readonly ApiPlatformDefinitionPolicy $definitionPolicy, + ) { + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->resourceClassResolver = $resourceClassResolver; + } + + /** + * @param array|null $serializerContext + * + * @return array{Type, Configuration}|null + */ + public function create(string $className, string $format, string $type, ?Operation $operation, ?array $serializerContext, string $version, bool $forceCollection = false): ?array + { + $operationWasProvided = null !== $operation; + + if (!$this->isResourceClass($className)) { + $serializerContext ??= $operation ? $this->getSerializerContext($operation, $type) : []; + $operation = null; + $inputOrOutputClass = $className; + } else { + $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format); + $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext); + if ($operationWasProvided || !($serializerContext[SchemaFactoryInterface::FORCE_SUBSCHEMA] ?? false)) { + $serializerContext = ($serializerContext ?? []) + $this->getSerializerContext($operation, $type); + } + } + + if (null === $inputOrOutputClass) { + return null; + } + + $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET'; + if (!$operation) { + $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET'; + } + + if (!($serializerContext[SchemaFactoryInterface::FORCE_SUBSCHEMA] ?? false) && Schema::TYPE_OUTPUT !== $type && !\in_array($method, ['POST', 'PATCH', 'PUT', 'QUERY'], true) && true !== $operation?->canDeserialize()) { + return null; + } + + $isJsonMergePatch = $this->definitionPolicy->isJsonMergePatch($operation, $type, $format); + + $validationGroups = $operation?->getValidationContext()['groups'] ?? null; + $attributes = $serializerContext[AbstractNormalizer::ATTRIBUTES] ?? null; + + $groups = array_values((array) ($serializerContext[AbstractNormalizer::GROUPS] ?? [])); + + $propertyOptions = ['schema_type' => $type, 'enable_getter_setter_extraction' => true]; + if (null !== $validationGroups) { + $propertyOptions['validation_groups'] = \is_array($validationGroups) ? $validationGroups : [$validationGroups]; + } + if ($ignoredAttributes = $serializerContext['ignored_attributes'] ?? null) { + $propertyOptions['ignored_attributes'] = $ignoredAttributes; + } + + $configuration = new Configuration( + dialect: $this->createDialect($version), + groups: $groups, + attributes: $attributes ? (array) $attributes : null, + ignoredAttributes: array_values((array) ($serializerContext['ignored_attributes'] ?? [])), + allowExtraAttributes: false !== ($serializerContext[AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES] ?? true), + validationGroups: $this->normalizeValidationGroups($validationGroups), + definitionName: $serializerContext[self::OPENAPI_DEFINITION_NAME] ?? null, + definitionPrefix: $this->definitionPolicy->createRootPrefix($operation, $className, $inputOrOutputClass), + format: $isJsonMergePatch ? 'merge-patch+json' : $format, + context: [ + self::CONTEXT_SCHEMA_TYPE => $type, + self::CONTEXT_FORMAT => $format, + self::CONTEXT_PARTIAL => $this->definitionPolicy->isPartialUpdate($operation, $type, $format), + self::CONTEXT_VERSION => $version, + self::CONTEXT_PROPERTY_OPTIONS => $propertyOptions, + self::CONTEXT_GROUPS => $groups, + self::CONTEXT_DESCRIPTION => $operation?->getDescription() ?: null, + self::CONTEXT_DEPRECATED => Schema::VERSION_SWAGGER !== $version && $operation && $operation->getDeprecationReason() && ($operationWasProvided || !($serializerContext[SchemaFactoryInterface::FORCE_SUBSCHEMA] ?? false)), + self::CONTEXT_EXTERNAL_DOCS => $operation instanceof HttpOperation ? ($operation->getTypes()[0] ?? null) : null, + ], + ); + + $objectType = Type::object($inputOrOutputClass); + $isCollection = $forceCollection || (!\in_array($method, ['POST', 'QUERY'], true) && $operation instanceof CollectionOperationInterface); + + return [$isCollection ? Type::list($objectType) : $objectType, $configuration]; + } + + private function createDialect(string $version): Dialect + { + return match ($version) { + Schema::VERSION_OPENAPI => new Dialect('#/components/schemas/', NullSyntax::Union), + Schema::VERSION_SWAGGER => new Dialect('#/definitions/', NullSyntax::Union), + default => new Dialect('#/definitions/', NullSyntax::Union, schemaUri: 'http://json-schema.org/draft-07/schema#'), + }; + } + + /** + * @return array|GroupSequence|null + */ + private function normalizeValidationGroups(mixed $groups): array|GroupSequence|null + { + return match (true) { + null === $groups, $groups instanceof \Closure, \is_array($groups) && \is_callable($groups) => null, + $groups instanceof GroupSequence => $groups, + \is_array($groups) => $groups, + default => [$groups], + }; + } +} diff --git a/src/JsonSchema/Generator/GeneratorSchemaFactory.php b/src/JsonSchema/Generator/GeneratorSchemaFactory.php new file mode 100644 index 00000000000..fadd3537865 --- /dev/null +++ b/src/JsonSchema/Generator/GeneratorSchemaFactory.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Symfony\Component\JsonSchema\ClassSchemaResolver\NativeClassSchemaResolver; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; +use Symfony\Component\JsonSchema\SchemaGenerator; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +final class GeneratorSchemaFactory implements SchemaFactoryInterface +{ + private readonly SchemaGenerator $generator; + + /** + * @param iterable $definitionProcessors + */ + public function __construct( + private readonly ConfigurationFactory $configurationFactory, + ApiPlatformDefinitionPolicy $definitionPolicy, + PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + PropertyMetadataFactoryInterface $propertyMetadataFactory, + ResourceClassResolverInterface $resourceClassResolver, + ?NameConverterInterface $nameConverter = null, + iterable $definitionProcessors = [], + ) { + $accessor = new PropertyMetadataAccessor($propertyNameCollectionFactory, $propertyMetadataFactory); + $this->generator = new SchemaGenerator( + new ApiPlatformPropertyInfoExtractor($accessor), + $definitionPolicy, + [new ResourceIriClassSchemaResolver($accessor, $resourceClassResolver), new NativeClassSchemaResolver()], + [new ApiPropertySchemaProvider($accessor)], + [new ApiPropertySchemaEnricher($accessor, $resourceClassResolver)], + [new PartialUpdateDefinitionProcessor($definitionPolicy), new NestedOperationDefinitionProcessor($definitionPolicy), ...$definitionProcessors], + $nameConverter, + ); + } + + public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema + { + $schema = $schema ? clone $schema : new Schema(); + + if (null === $created = $this->configurationFactory->create($className, $format, $type, $operation, $serializerContext, $schema->getVersion(), $forceCollection)) { + return $schema; + } + + [$rootType, $configuration] = $created; + $context = $configuration->context; + + $generated = $this->generator->generate($rootType, $configuration); + $refPath = $configuration->dialect->refPath; + + $root = $generated->getRoot(); + $rootName = $root['items']['$ref'] ?? $root['$ref'] ?? null; + $rootName = \is_string($rootName) && str_starts_with($rootName, $refPath) ? substr($rootName, \strlen($refPath)) : null; + + foreach ($root as $keyword => $value) { + $schema[$keyword] = $value; + } + + $definitions = $schema->getDefinitions(); + foreach ($generated->getDefinitions() as $name => $definition) { + if ($name === $rootName) { + $definition += array_filter([ + 'description' => $context[ConfigurationFactory::CONTEXT_DESCRIPTION], + 'deprecated' => $context[ConfigurationFactory::CONTEXT_DEPRECATED] ?: null, + 'externalDocs' => $context[ConfigurationFactory::CONTEXT_EXTERNAL_DOCS] ? ['url' => $context[ConfigurationFactory::CONTEXT_EXTERNAL_DOCS]] : null, + ]); + } + + foreach ($definition['allOf'] ?? [] as $key => $member) { + if (isset($member['properties'])) { + $definition['allOf'][$key] = self::toDefinitionObject($member); + } + } + + $definitions[$name] = self::toDefinitionObject($definition); + } + + return $schema; + } + + /** + * @param array $definition + */ + private static function toDefinitionObject(array $definition): \ArrayObject + { + foreach ($definition['properties'] ?? [] as $property => $propertySchema) { + $definition['properties'][$property] = $propertySchema instanceof \ArrayObject ? $propertySchema : new \ArrayObject($propertySchema); + } + + return new \ArrayObject($definition); + } +} diff --git a/src/JsonSchema/Generator/NestedOperationDefinitionProcessor.php b/src/JsonSchema/Generator/NestedOperationDefinitionProcessor.php new file mode 100644 index 00000000000..b8cfcd582d1 --- /dev/null +++ b/src/JsonSchema/Generator/NestedOperationDefinitionProcessor.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\Metadata\HttpOperation; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\DefinitionParent; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; +use Symfony\Component\JsonSchema\SchemaDocument; + +final class NestedOperationDefinitionProcessor implements DefinitionProcessorInterface +{ + public function __construct(private readonly ApiPlatformDefinitionPolicy $definitionPolicy) + { + } + + public function process(array $definition, string $class, Configuration $config, SchemaDocument $document, ?DefinitionParent $parent): array + { + if (null === $parent || null === $operation = $this->definitionPolicy->findNestedOperation($class, $config)) { + return $definition; + } + + if ($description = $operation->getDescription()) { + $definition['description'] = $description; + } + + if ($operation instanceof HttpOperation && ($type = $operation->getTypes()[0] ?? null)) { + $definition['externalDocs'] = ['url' => $type]; + } + + return $definition; + } +} diff --git a/src/JsonSchema/Generator/PartialUpdateDefinitionProcessor.php b/src/JsonSchema/Generator/PartialUpdateDefinitionProcessor.php new file mode 100644 index 00000000000..282ab274888 --- /dev/null +++ b/src/JsonSchema/Generator/PartialUpdateDefinitionProcessor.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\DefinitionParent; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; +use Symfony\Component\JsonSchema\SchemaDocument; + +final class PartialUpdateDefinitionProcessor implements DefinitionProcessorInterface +{ + public function __construct(private readonly ApiPlatformDefinitionPolicy $definitionPolicy) + { + } + + public function process(array $definition, string $class, Configuration $config, SchemaDocument $document, ?DefinitionParent $parent): array + { + if ($this->definitionPolicy->isPartial($class, $config, $parent)) { + unset($definition['required']); + } + + return $definition; + } +} diff --git a/src/JsonSchema/Generator/PropertyMetadataAccessor.php b/src/JsonSchema/Generator/PropertyMetadataAccessor.php new file mode 100644 index 00000000000..80ac92652a8 --- /dev/null +++ b/src/JsonSchema/Generator/PropertyMetadataAccessor.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use Symfony\Component\JsonSchema\Configuration; + +final class PropertyMetadataAccessor +{ + public function __construct( + private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, + ) { + } + + /** + * @return array + */ + public static function contextFromConfiguration(Configuration $configuration): array + { + $context = [ConfigurationFactory::CONTEXT_PROPERTY_OPTIONS => $configuration->context[ConfigurationFactory::CONTEXT_PROPERTY_OPTIONS] ?? []]; + if ($configuration->groups) { + $context['serializer_groups'] = $configuration->groups; + } + if (null !== $configuration->attributes) { + $context['serializer_attributes'] = $configuration->attributes; + } + + return $context; + } + + /** + * @param class-string $class + * @param array $context + * + * @return list + */ + public function getNames(string $class, array $context): array + { + return array_values(iterator_to_array($this->propertyNameCollectionFactory->create($class, self::options($context)), false)); + } + + /** + * @param class-string $class + * @param array $context + */ + public function get(string $class, string $property, array $context): ApiProperty + { + return $this->propertyMetadataFactory->create($class, $property, self::options($context)); + } + + /** + * @param array $context + * + * @return array + */ + private static function options(array $context): array + { + $options = $context[ConfigurationFactory::CONTEXT_PROPERTY_OPTIONS] ?? []; + $context = array_diff_key($context, array_flip(ConfigurationFactory::CONTEXT_KEYS)); + if (null === ($context['serializer_groups'] ?? null)) { + unset($context['serializer_groups']); + } + + return $options + $context; + } +} diff --git a/src/JsonSchema/Generator/ResourceIriClassSchemaResolver.php b/src/JsonSchema/Generator/ResourceIriClassSchemaResolver.php new file mode 100644 index 00000000000..1045f0f6b51 --- /dev/null +++ b/src/JsonSchema/Generator/ResourceIriClassSchemaResolver.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Generator; + +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Symfony\Component\JsonSchema\ClassSchemaResolver\ClassSchemaResolverInterface; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\DefinitionParent; +use Symfony\Component\JsonSchema\SchemaDocument; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\ObjectType; + +final class ResourceIriClassSchemaResolver implements ClassSchemaResolverInterface +{ + public function __construct( + private readonly PropertyMetadataAccessor $accessor, + private readonly ResourceClassResolverInterface $resourceClassResolver, + ) { + } + + public function resolve(string $class, Configuration $config, SchemaDocument $document, ?DefinitionParent $parent = null): ?array + { + if (null === $parent || !$this->resourceClassResolver->isResourceClass($class)) { + return null; + } + + $metadata = $this->accessor->get($parent->class, $parent->property, PropertyMetadataAccessor::contextFromConfiguration($parent->config)); + + if ($this->isLink($parent->class, $metadata, $config->context[ConfigurationFactory::CONTEXT_SCHEMA_TYPE])) { + return null; + } + + return ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/']; + } + + private function isLink(string $class, ApiProperty $metadata, string $schemaType): bool + { + $isInput = Schema::TYPE_INPUT === $schemaType; + $link = $isInput ? $metadata->isWritableLink() : ($metadata->isReadableLink() || false === $metadata->getGenId()); + + if (!$isInput && !$this->resourceClassResolver->isResourceClass($class) && !$metadata->getNativeType()?->isSatisfiedBy($this->isResourceType(...))) { + return true; + } + + return $link ?? true; + } + + private function isResourceType(Type $type): bool + { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($type->getClassName()); + } +} diff --git a/src/JsonSchema/composer.json b/src/JsonSchema/composer.json index 713b0990392..574e79d2025 100644 --- a/src/JsonSchema/composer.json +++ b/src/JsonSchema/composer.json @@ -36,6 +36,9 @@ "phpspec/prophecy-phpunit": "^2.2", "phpunit/phpunit": "^11.5 || ^12.2 || ^13.0" }, + "suggest": { + "symfony/json-schema": "To generate JSON Schemas with the Symfony JsonSchema component (opt-in, api_platform.json_schema.generator)." + }, "autoload": { "psr-4": { "ApiPlatform\\JsonSchema\\": "" diff --git a/src/Metadata/AsJsonSchemaDefinitionProcessor.php b/src/Metadata/AsJsonSchemaDefinitionProcessor.php new file mode 100644 index 00000000000..88a00ef9227 --- /dev/null +++ b/src/Metadata/AsJsonSchemaDefinitionProcessor.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Metadata; + +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsJsonSchemaDefinitionProcessor +{ +} diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 49f20f828f2..5bfcc39f1b6 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -33,6 +33,7 @@ use ApiPlatform\HttpCache\PurgerInterface; use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\AsJsonSchemaDefinitionProcessor; use ApiPlatform\Metadata\AsOperationMutator; use ApiPlatform\Metadata\AsResourceMutator; use ApiPlatform\Metadata\Delete; @@ -84,6 +85,8 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Finder\Finder; use Symfony\Component\HttpClient\ScopingHttpClient; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; +use Symfony\Component\JsonSchema\SchemaGenerator; use Symfony\Component\JsonStreamer\JsonStreamWriter; use Symfony\Component\ObjectMapper\ObjectMapper; use Symfony\Component\ObjectMapper\ObjectMapperInterface; @@ -236,6 +239,19 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerForAutoconfiguration(ParameterProviderInterface::class) ->addTag('api_platform.parameter_provider'); + if (interface_exists(DefinitionProcessorInterface::class)) { + $container->registerAttributeForAutoconfiguration( + AsJsonSchemaDefinitionProcessor::class, + static function (ChildDefinition $definition, AsJsonSchemaDefinitionProcessor $attribute, \ReflectionClass $reflector): void { // @phpstan-ignore-line + if (!is_a($reflector->name, DefinitionProcessorInterface::class, true)) { + throw new RuntimeException(\sprintf('JSON Schema definition processor "%s" should implement %s', $reflector->name, DefinitionProcessorInterface::class)); + } + + $definition->addTag('api_platform.json_schema.definition_processor'); + }, + ); + } + $container->registerAttributeForAutoconfiguration( AsResourceMutator::class, static function (ChildDefinition $definition, AsResourceMutator $attribute, \ReflectionClass $reflector): void { // @phpstan-ignore-line @@ -747,6 +763,10 @@ private function registerJsonLdHydraConfiguration(ContainerBuilder $container, a if (!$container->has('api_platform.json_schema.schema_factory')) { $container->removeDefinition('api_platform.hydra.json_schema.schema_factory'); } + + if (!$config['json_schema']['generator']) { + $container->removeDefinition('api_platform.hydra.json_schema.item_definition_processor'); + } } private function registerJsonHalConfiguration(array $formats, PhpFileLoader $loader): void @@ -1112,6 +1132,16 @@ private function registerOpenApiConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.openapi.validationErrorResourceClass', $config['openapi']['validation_error_resource_class'] ?? null); $loader->load('json_schema.php'); + + if (!$config['json_schema']['generator']) { + return; + } + + if (!class_exists(SchemaGenerator::class)) { + throw new \LogicException('The "api_platform.json_schema.generator" option requires the Symfony JsonSchema component. Try running "composer require symfony/json-schema".'); + } + + $loader->load('json_schema_generator.php'); } private function registerMakerConfiguration(ContainerBuilder $container, array $config, PhpFileLoader $loader): void diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 10db1dde0dc..c3f04c23121 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -215,6 +215,12 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultValue([]) ->info('The JSON formats to compute the JSON Schemas for.') ->end() + ->arrayNode('json_schema') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('generator')->defaultFalse()->info('Build JSON Schemas with the Symfony JsonSchema component (experimental, requires symfony/json-schema).')->end() + ->end() + ->end() ->end(); $this->addDefaultsSection($rootNode); diff --git a/src/Symfony/Bundle/Resources/config/hydra.php b/src/Symfony/Bundle/Resources/config/hydra.php index 1ce0d98c9d5..4a808d25810 100644 --- a/src/Symfony/Bundle/Resources/config/hydra.php +++ b/src/Symfony/Bundle/Resources/config/hydra.php @@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use ApiPlatform\Hydra\JsonSchema\ItemDefinitionProcessor; use ApiPlatform\Hydra\JsonSchema\SchemaFactory; use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer; use ApiPlatform\Hydra\Serializer\CollectionNormalizer; @@ -35,6 +36,13 @@ service('api_platform.metadata.resource.metadata_collection_factory')->ignoreOnInvalid(), ]); + $services->set('api_platform.hydra.json_schema.item_definition_processor', ItemDefinitionProcessor::class) + ->args([ + service('api_platform.metadata.resource.metadata_collection_factory'), + service('api_platform.json_schema.generator.definition_policy'), + ]) + ->tag('api_platform.json_schema.definition_processor'); + $services->set('api_platform.hydra.normalizer.documentation', DocumentationNormalizer::class) ->args([ service('api_platform.metadata.resource.metadata_collection_factory'), diff --git a/src/Symfony/Bundle/Resources/config/json_schema_generator.php b/src/Symfony/Bundle/Resources/config/json_schema_generator.php new file mode 100644 index 00000000000..769474a1cea --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/json_schema_generator.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use ApiPlatform\JsonSchema\Generator\ApiPlatformDefinitionPolicy; +use ApiPlatform\JsonSchema\Generator\ConfigurationFactory; +use ApiPlatform\JsonSchema\Generator\GeneratorSchemaFactory; + +return static function (ContainerConfigurator $container) { + $services = $container->services(); + + $services->set('api_platform.json_schema.generator.definition_policy', ApiPlatformDefinitionPolicy::class) + ->args([ + service('api_platform.metadata.resource.metadata_collection_factory'), + service('api_platform.resource_class_resolver'), + service('api_platform.metadata.property.metadata_factory'), + ]); + + $services->set('api_platform.json_schema.generator.configuration_factory', ConfigurationFactory::class) + ->args([ + service('api_platform.metadata.resource.metadata_collection_factory'), + service('api_platform.resource_class_resolver'), + service('api_platform.json_schema.generator.definition_policy'), + ]); + + $services->set('api_platform.json_schema.schema_factory', GeneratorSchemaFactory::class) + ->args([ + service('api_platform.json_schema.generator.configuration_factory'), + service('api_platform.json_schema.generator.definition_policy'), + service('api_platform.metadata.property.name_collection_factory'), + service('api_platform.metadata.property.metadata_factory'), + service('api_platform.resource_class_resolver'), + service('api_platform.name_converter')->ignoreOnInvalid(), + tagged_iterator('api_platform.json_schema.definition_processor'), + ]); +}; diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 0f3fe8a6099..de512e77e5a 100644 --- a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -13,6 +13,12 @@ namespace ApiPlatform\Symfony\Tests\Bundle\DependencyInjection; +use ApiPlatform\Hydra\JsonSchema\ItemDefinitionProcessor; +use ApiPlatform\JsonSchema\Generator\ApiPlatformDefinitionPolicy; +use ApiPlatform\JsonSchema\Generator\ConfigurationFactory; +use ApiPlatform\JsonSchema\Generator\GeneratorSchemaFactory; +use ApiPlatform\JsonSchema\SchemaFactory; +use ApiPlatform\Metadata\AsJsonSchemaDefinitionProcessor; use ApiPlatform\Metadata\Exception\ExceptionInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IdentifiersExtractorInterface; @@ -26,6 +32,9 @@ use ApiPlatform\State\SerializerContextBuilderInterface; use ApiPlatform\Symfony\Action\NotFoundAction; use ApiPlatform\Symfony\Bundle\DependencyInjection\ApiPlatformExtension; +use ApiPlatform\Symfony\Tests\Fixtures\JsonSchemaProcessor\InvalidDefinitionProcessor; +use ApiPlatform\Symfony\Tests\Fixtures\JsonSchemaProcessor\OptedInDefinitionProcessor; +use ApiPlatform\Symfony\Tests\Fixtures\JsonSchemaProcessor\PlainDefinitionProcessor; use ApiPlatform\Tests\Fixtures\TestBundle\TestBundle; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\ORM\OptimisticLockException; @@ -36,10 +45,13 @@ use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand as FrameworkTranslationExtractCommand; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Translation\Command\TranslationExtractCommand; @@ -469,6 +481,92 @@ public function testMcpProviderChainIsNotSecuredWithoutSecurityBundle(): void $this->assertContainerHasService('api_platform.mcp.state_provider.parameter_validator'); } + public function testJsonSchemaGeneratorIsDisabledByDefault(): void + { + $this->skipWithoutJsonSchemaComponent(); + (new ApiPlatformExtension())->load(self::DEFAULT_CONFIG, $this->container); + + $this->assertSame(SchemaFactory::class, $this->container->getDefinition('api_platform.json_schema.schema_factory')->getClass()); + $this->assertNotContainerHasService('api_platform.json_schema.generator.definition_policy'); + $this->assertNotContainerHasService('api_platform.json_schema.generator.configuration_factory'); + $this->assertNotContainerHasService('api_platform.hydra.json_schema.item_definition_processor'); + $this->assertSame([], $this->container->findTaggedServiceIds('api_platform.json_schema.definition_processor')); + } + + public function testJsonSchemaGeneratorReplacesTheBaseSchemaFactory(): void + { + $this->skipWithoutJsonSchemaComponent(); + $config = self::DEFAULT_CONFIG; + $config['api_platform']['json_schema']['generator'] = true; + (new ApiPlatformExtension())->load($config, $this->container); + + $schemaFactory = $this->container->getDefinition('api_platform.json_schema.schema_factory'); + $this->assertSame(GeneratorSchemaFactory::class, $schemaFactory->getClass()); + $this->assertSame(ApiPlatformDefinitionPolicy::class, $this->container->getDefinition('api_platform.json_schema.generator.definition_policy')->getClass()); + $this->assertTrue($this->container->getDefinition('api_platform.json_schema.generator.definition_policy')->isShared()); + $this->assertSame(ConfigurationFactory::class, $this->container->getDefinition('api_platform.json_schema.generator.configuration_factory')->getClass()); + + $definitionProcessors = $schemaFactory->getArgument(6); + $this->assertInstanceOf(TaggedIteratorArgument::class, $definitionProcessors); + $this->assertSame('api_platform.json_schema.definition_processor', $definitionProcessors->getTag()); + + $this->assertSame(ItemDefinitionProcessor::class, $this->container->getDefinition('api_platform.hydra.json_schema.item_definition_processor')->getClass()); + $this->assertServiceHasTags('api_platform.hydra.json_schema.item_definition_processor', ['api_platform.json_schema.definition_processor']); + + $this->assertSame('api_platform.json_schema.schema_factory', $this->container->getDefinition('api_platform.hydra.json_schema.schema_factory')->getDecoratedService()[0]); + $this->assertSame('api_platform.json_schema.schema_factory', $this->container->getDefinition('api_platform.hal.json_schema.schema_factory')->getDecoratedService()[0]); + $this->assertSame('api_platform.json_schema.schema_factory', $this->container->getDefinition('api_platform.json_schema.backward_compatible_schema_factory')->getDecoratedService()[0]); + } + + public function testJsonSchemaDefinitionProcessorsAreNotAutoconfiguredFromTheInterface(): void + { + $this->skipWithoutJsonSchemaComponent(); + (new ApiPlatformExtension())->load(self::DEFAULT_CONFIG, $this->container); + + $this->assertArrayNotHasKey(DefinitionProcessorInterface::class, $this->container->getAutoconfiguredInstanceof()); + } + + public function testJsonSchemaDefinitionProcessorAttributeAddsTheTag(): void + { + $this->skipWithoutJsonSchemaComponent(); + (new ApiPlatformExtension())->load(self::DEFAULT_CONFIG, $this->container); + + $definition = new ChildDefinition(''); + $this->applyAttributeAutoconfiguration($definition, OptedInDefinitionProcessor::class); + $this->assertArrayHasKey('api_platform.json_schema.definition_processor', $definition->getTags()); + + $plain = new ChildDefinition(''); + $this->applyAttributeAutoconfiguration($plain, PlainDefinitionProcessor::class); + $this->assertSame([], $plain->getTags()); + } + + public function testJsonSchemaDefinitionProcessorAttributeRequiresTheInterface(): void + { + $this->skipWithoutJsonSchemaComponent(); + (new ApiPlatformExtension())->load(self::DEFAULT_CONFIG, $this->container); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(\sprintf('should implement %s', DefinitionProcessorInterface::class)); + $this->applyAttributeAutoconfiguration(new ChildDefinition(''), InvalidDefinitionProcessor::class); + } + + private function skipWithoutJsonSchemaComponent(): void + { + if (!interface_exists(DefinitionProcessorInterface::class)) { + $this->markTestSkipped('symfony/json-schema is not installed.'); + } + } + + private function applyAttributeAutoconfiguration(ChildDefinition $definition, string $class): void + { + $reflector = new \ReflectionClass($class); + foreach ($reflector->getAttributes(AsJsonSchemaDefinitionProcessor::class) as $attribute) { + foreach ($this->container->getAttributeAutoconfigurators()[AsJsonSchemaDefinitionProcessor::class] ?? [] as $configurator) { + $configurator($definition, $attribute->newInstance(), $reflector); + } + } + } + public function testItRegistersMetadataConfiguration(): void { $config = self::DEFAULT_CONFIG; @@ -553,7 +651,7 @@ public function testPropertyInfoExtractorsDoNotLeakIntoFrameworkPropertyInfo(): $apiPlatformPropertyInfo = $this->container->getDefinition('api_platform.property_info'); foreach ($apiPlatformPropertyInfo->getArguments() as $arg) { - if ($arg instanceof \Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument) { + if ($arg instanceof TaggedIteratorArgument) { $this->assertStringStartsWith('api_platform.property_info.', $arg->getTag(), \sprintf('api_platform.property_info must consume only "api_platform.property_info.*" private tags; found "%s".', $arg->getTag())); } } diff --git a/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/InvalidDefinitionProcessor.php b/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/InvalidDefinitionProcessor.php new file mode 100644 index 00000000000..af51754f9ab --- /dev/null +++ b/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/InvalidDefinitionProcessor.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\Fixtures\JsonSchemaProcessor; + +use ApiPlatform\Metadata\AsJsonSchemaDefinitionProcessor; + +#[AsJsonSchemaDefinitionProcessor] +final class InvalidDefinitionProcessor +{ +} diff --git a/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/OptedInDefinitionProcessor.php b/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/OptedInDefinitionProcessor.php new file mode 100644 index 00000000000..f3a06643dfa --- /dev/null +++ b/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/OptedInDefinitionProcessor.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\Fixtures\JsonSchemaProcessor; + +use ApiPlatform\Metadata\AsJsonSchemaDefinitionProcessor; +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\DefinitionParent; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; +use Symfony\Component\JsonSchema\SchemaDocument; + +#[AsJsonSchemaDefinitionProcessor] +final class OptedInDefinitionProcessor implements DefinitionProcessorInterface +{ + public function process(array $definition, string $class, Configuration $config, SchemaDocument $document, ?DefinitionParent $parent): array + { + return $definition; + } +} diff --git a/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/PlainDefinitionProcessor.php b/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/PlainDefinitionProcessor.php new file mode 100644 index 00000000000..40350b6e630 --- /dev/null +++ b/src/Symfony/Tests/Fixtures/JsonSchemaProcessor/PlainDefinitionProcessor.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\Fixtures\JsonSchemaProcessor; + +use Symfony\Component\JsonSchema\Configuration; +use Symfony\Component\JsonSchema\DefinitionParent; +use Symfony\Component\JsonSchema\DefinitionProcessor\DefinitionProcessorInterface; +use Symfony\Component\JsonSchema\SchemaDocument; + +final class PlainDefinitionProcessor implements DefinitionProcessorInterface +{ + public function process(array $definition, string $class, Configuration $config, SchemaDocument $document, ?DefinitionParent $parent): array + { + return $definition; + } +} diff --git a/src/Symfony/composer.json b/src/Symfony/composer.json index cc756a947c1..f8a39914b6b 100644 --- a/src/Symfony/composer.json +++ b/src/Symfony/composer.json @@ -88,6 +88,7 @@ "symfony/security": "To use authorization features.", "symfony/twig-bundle": "To use the Swagger UI integration.", "symfony/uid": "To support Symfony UUID/ULID identifiers.", + "symfony/json-schema": "To generate JSON Schemas with the Symfony JsonSchema component (opt-in, api_platform.json_schema.generator).", "symfony/messenger": "To support messenger integration and asynchronous Mercure updates.", "symfony/mercure-bundle": "To support mercure integration.", "symfony/web-profiler-bundle": "To use the data collector." diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/AttributeSelectedAuthor.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/AttributeSelectedAuthor.php new file mode 100644 index 00000000000..0ff7e74a79e --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/AttributeSelectedAuthor.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; + +#[ApiResource( + shortName: 'AttributeSelectedAuthor', + operations: [ + new Get( + uriTemplate: '/attribute_selected_authors/{id}', + name: 'attribute_selected_author_get', + provider: [self::class, 'provide'], + ), + ], +)] +final class AttributeSelectedAuthor +{ + #[ApiProperty(identifier: true)] + public ?int $id = null; + + public string $name = ''; + + public string $email = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/AttributeSelectedBook.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/AttributeSelectedBook.php new file mode 100644 index 00000000000..7b912202c86 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/AttributeSelectedBook.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; + +#[ApiResource( + shortName: 'AttributeSelectedBook', + operations: [ + new Get( + uriTemplate: '/attribute_selected_books/{id}', + name: 'attribute_selected_book_get', + normalizationContext: ['attributes' => ['title', 'author' => ['name']]], + provider: [self::class, 'provide'], + ), + new Post( + uriTemplate: '/attribute_selected_books', + name: 'attribute_selected_book_post', + denormalizationContext: ['attributes' => ['title', 'author' => ['name']]], + processor: [self::class, 'process'], + ), + ], +)] +final class AttributeSelectedBook +{ + #[ApiProperty(identifier: true)] + public ?int $id = null; + + public string $title = ''; + + public string $isbn = ''; + + public ?AttributeSelectedAuthor $author = null; + + public static function provide(): null + { + return null; + } + + public static function process(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/BuiltinTypeQuirks.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/BuiltinTypeQuirks.php new file mode 100644 index 00000000000..5930313edd3 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/BuiltinTypeQuirks.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use BcMath\Number; + +#[ApiResource( + shortName: 'BuiltinTypeQuirks', + operations: [ + new Get(uriTemplate: '/builtin_type_quirks/{id}', name: 'builtin_type_quirks_get'), + ], +)] +final class BuiltinTypeQuirks +{ + public ?int $id = null; + + public mixed $anything = null; + + public array $untyped = []; + + public ?Number $amount = null; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenActivable.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenActivable.php new file mode 100644 index 00000000000..55274c3c5f0 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenActivable.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +interface GoldenActivable +{ +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenAuthor.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenAuthor.php new file mode 100644 index 00000000000..fa33e6e2827 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenAuthor.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use Symfony\Component\Serializer\Attribute\Groups; + +final class GoldenAuthor +{ + #[Groups(['golden:read'])] + public string $name = ''; + + #[Groups(['golden:read'])] + public string $email = ''; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenBook.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenBook.php new file mode 100644 index 00000000000..7707fb37ecc --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenBook.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenBook', + operations: [ + new Get( + uriTemplate: '/golden_books/{id}', + name: 'golden_book_get', + description: 'Retrieves a golden book.', + deprecationReason: 'Use another book endpoint.', + types: ['https://schema.org/Book'], + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + new GetCollection( + uriTemplate: '/golden_books', + name: 'golden_book_get_collection', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenBook +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[Groups(['golden:read'])] + public string $title = ''; + + #[ApiProperty(description: 'International standard book number.', deprecationReason: 'Use title.', example: '9780000000000')] + #[Groups(['golden:read'])] + public ?string $isbn = null; + + public string $internalNote = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenDescribedOwner.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenDescribedOwner.php new file mode 100644 index 00000000000..1385fc2c68b --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenDescribedOwner.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; + +#[ApiResource( + shortName: 'GoldenDescribedOwner', + operations: [ + new Get( + uriTemplate: '/golden_described_owners/{id}', + name: 'golden_described_owner_get', + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenDescribedOwner +{ + #[ApiProperty(identifier: true)] + public ?int $id = null; + + #[ApiProperty(readableLink: true)] + public ?GoldenDescribedRelated $related = null; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenDescribedRelated.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenDescribedRelated.php new file mode 100644 index 00000000000..42f1e58fcd4 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenDescribedRelated.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; + +#[ApiResource( + shortName: 'GoldenDescribedAlias', + description: 'A described related resource.', + types: ['https://schema.org/Thing'], + operations: [ + new Get( + uriTemplate: '/golden_described_related/{id}', + name: 'golden_described_related_get', + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenDescribedRelated +{ + #[ApiProperty(identifier: true)] + public ?int $id = null; + + public string $label = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenGroupedItem.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenGroupedItem.php new file mode 100644 index 00000000000..70a45b1ff64 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenGroupedItem.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenGroupedItem', + operations: [ + new Get( + uriTemplate: '/golden_grouped_items/{id}', + name: 'golden_grouped_item_get', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenGroupedItem +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[Groups(['golden:read', 'golden:write'])] + public string $title = ''; + + #[Groups(['golden:read'])] + public ?string $createdAt = null; + + #[Groups(['golden:write'])] + public ?string $secretToken = null; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenInputResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenInputResource.php new file mode 100644 index 00000000000..83fd6d1144b --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenInputResource.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + shortName: 'GoldenInput', + operations: [ + new Post( + uriTemplate: '/golden_inputs', + name: 'golden_input_post', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + processor: [self::class, 'process'], + ), + new Patch( + uriTemplate: '/golden_inputs/{id}', + name: 'golden_input_patch', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + provider: [self::class, 'process'], + processor: [self::class, 'process'], + ), + new Put( + uriTemplate: '/golden_inputs_put/{id}', + name: 'golden_input_put_nonstandard', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + extraProperties: ['standard_put' => false], + provider: [self::class, 'process'], + processor: [self::class, 'process'], + ), + ], +)] +final class GoldenInputResource +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[Assert\NotBlank] + #[Groups(['golden:read', 'golden:write'])] + public string $title = ''; + + #[Groups(['golden:read'])] + public ?string $createdAt = null; + + #[ApiProperty(writable: false)] + #[Groups(['golden:write'])] + public ?string $computedLabel = null; + + #[ApiProperty(readable: false)] + #[Groups(['golden:write'])] + public ?string $password = null; + + public static function process(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenIntersectionResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenIntersectionResource.php new file mode 100644 index 00000000000..4f93bfb460d --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenIntersectionResource.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; + +#[ApiResource( + shortName: 'GoldenIntersection', + operations: [ + new Get(uriTemplate: '/golden_intersections/{id}', name: 'golden_intersection_get'), + ], +)] +final class GoldenIntersectionResource +{ + public ?int $id = null; + + public GoldenActivable&GoldenTimestampable $library; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenMultiResourceEntity.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenMultiResourceEntity.php new file mode 100644 index 00000000000..c492a316a9d --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenMultiResourceEntity.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenAdminMultiResource', + operations: [ + new Get( + uriTemplate: '/golden_admin_multi_resources/{id}', + name: 'golden_admin_multi_resource_get', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + ], +)] +#[ApiResource( + shortName: 'GoldenMultiResource', + operations: [ + new Get( + uriTemplate: '/golden_multi_resources/{id}', + name: 'golden_multi_resource_get', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + new Post( + uriTemplate: '/golden_multi_resources', + name: 'golden_multi_resource_post', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + processor: [self::class, 'provide'], + ), + ], +)] +final class GoldenMultiResourceEntity +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[Groups(['golden:read', 'golden:write'])] + public string $label = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenMultiResourceOwner.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenMultiResourceOwner.php new file mode 100644 index 00000000000..a37e59b3f2c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenMultiResourceOwner.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenMultiResourceOwner', + operations: [ + new Get( + uriTemplate: '/golden_multi_resource_owners/{id}', + name: 'golden_multi_resource_owner_get', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + new Post( + uriTemplate: '/golden_multi_resource_owners', + name: 'golden_multi_resource_owner_post', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + processor: [self::class, 'provide'], + ), + ], +)] +final class GoldenMultiResourceOwner +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[ApiProperty(readableLink: true, writableLink: true)] + #[Groups(['golden:read', 'golden:write'])] + public ?GoldenMultiResourceEntity $entity = null; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenNestedResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenNestedResource.php new file mode 100644 index 00000000000..817a2233f2f --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenNestedResource.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; + +#[ApiResource( + shortName: 'GoldenNested', + operations: [ + new Get( + uriTemplate: '/golden_nested/{id}', + name: 'golden_nested_get_attributes', + normalizationContext: ['attributes' => ['name', 'author' => ['name']]], + provider: [self::class, 'provide'], + ), + new Get( + uriTemplate: '/golden_nested_ignored/{id}', + name: 'golden_nested_get_ignored', + normalizationContext: ['ignored_attributes' => ['secret']], + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenNestedResource +{ + #[ApiProperty(identifier: true)] + public ?int $id = null; + + public string $name = ''; + + public string $secret = ''; + + public ?GoldenAuthor $author = null; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenNonStandardPutChild.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenNonStandardPutChild.php new file mode 100644 index 00000000000..7632772d077 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenNonStandardPutChild.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Put; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenNonStandardPutChild', + operations: [ + new Get( + uriTemplate: '/golden_non_standard_put_children/{id}', + name: 'golden_non_standard_put_child_get', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + new Put( + uriTemplate: '/golden_non_standard_put_children/{id}', + name: 'golden_non_standard_put_child_put', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + extraProperties: ['standard_put' => false], + provider: [self::class, 'provide'], + processor: [self::class, 'provide'], + ), + ], +)] +final class GoldenNonStandardPutChild +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[ApiProperty(required: true)] + #[Groups(['golden:read', 'golden:write'])] + public string $label = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDto.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDto.php new file mode 100644 index 00000000000..e0537438c3e --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDto.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +final class GoldenOutputDto +{ + public string $label = ''; + + public int $total = 0; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDtoOwner.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDtoOwner.php new file mode 100644 index 00000000000..d88f3f9434f --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDtoOwner.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; + +#[ApiResource( + shortName: 'GoldenOutputDtoOwner', + operations: [ + new Get( + uriTemplate: '/golden_output_dto_owners/{id}', + name: 'golden_output_dto_owner_get', + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenOutputDtoOwner +{ + #[ApiProperty(identifier: true)] + public ?int $id = null; + + #[ApiProperty(readableLink: true)] + public ?GoldenOutputDtoResource $dto = null; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDtoResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDtoResource.php new file mode 100644 index 00000000000..ee63a3658be --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenOutputDtoResource.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; + +#[ApiResource( + shortName: 'GoldenOutputDtoResource', + operations: [ + new Get( + uriTemplate: '/golden_output_dtos/{id}', + name: 'golden_output_dto_get', + output: GoldenOutputDto::class, + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenOutputDtoResource +{ + public ?int $id = null; + + public string $internal = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPartialChildrenOwner.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPartialChildrenOwner.php new file mode 100644 index 00000000000..31fd7129e85 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPartialChildrenOwner.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenPartialChildrenOwner', + operations: [ + new Get( + uriTemplate: '/golden_partial_children_owners/{id}', + name: 'golden_partial_children_owner_get', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + new Post( + uriTemplate: '/golden_partial_children_owners', + name: 'golden_partial_children_owner_post', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + processor: [self::class, 'provide'], + ), + ], +)] +final class GoldenPartialChildrenOwner +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[ApiProperty(required: true)] + #[Groups(['golden:read', 'golden:write'])] + public string $name = ''; + + #[ApiProperty(readableLink: true, writableLink: true)] + #[Groups(['golden:read', 'golden:write'])] + public ?GoldenPatchOnlyChild $patchChild = null; + + #[ApiProperty(readableLink: true, writableLink: true)] + #[Groups(['golden:read', 'golden:write'])] + public ?GoldenNonStandardPutChild $putChild = null; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPatchOnlyChild.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPatchOnlyChild.php new file mode 100644 index 00000000000..25e49115d84 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPatchOnlyChild.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Patch; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenPatchOnlyChild', + operations: [ + new Get( + uriTemplate: '/golden_patch_only_children/{id}', + name: 'golden_patch_only_child_get', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + new Patch( + uriTemplate: '/golden_patch_only_children/{id}', + name: 'golden_patch_only_child_patch', + normalizationContext: ['groups' => ['golden:read']], + denormalizationContext: ['groups' => ['golden:write']], + provider: [self::class, 'provide'], + processor: [self::class, 'provide'], + ), + ], +)] +final class GoldenPatchOnlyChild +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[ApiProperty(required: true)] + #[Groups(['golden:read', 'golden:write'])] + public string $label = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPlainObject.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPlainObject.php new file mode 100644 index 00000000000..9105e1190b6 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenPlainObject.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use Symfony\Component\Serializer\Attribute\Groups; + +final class GoldenPlainObject +{ + public string $name = ''; + + public ?int $count = null; + + #[Groups(['golden:read'])] + public bool $flag = false; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenQueryCriteria.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenQueryCriteria.php new file mode 100644 index 00000000000..464e20ca64e --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenQueryCriteria.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use Symfony\Component\Validator\Constraints as Assert; + +final class GoldenQueryCriteria +{ + #[Assert\NotBlank] + public string $term = ''; + + public ?int $limit = null; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenQueryResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenQueryResource.php new file mode 100644 index 00000000000..fe5a81cf6e7 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenQueryResource.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Query; + +#[ApiResource( + shortName: 'GoldenQuery', + operations: [ + new Query( + uriTemplate: '/golden_query', + name: 'golden_query', + input: GoldenQueryCriteria::class, + read: false, + deserialize: true, + write: true, + processor: [self::class, 'process'], + ), + ], +)] +final class GoldenQueryResource +{ + public ?int $id = null; + + public string $title = ''; + + public static function process(mixed $data): mixed + { + return $data; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenRelated.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenRelated.php new file mode 100644 index 00000000000..935db35e50c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenRelated.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenRelated', + operations: [ + new Get( + uriTemplate: '/golden_related/{id}', + name: 'golden_related_get', + normalizationContext: ['groups' => ['golden:read']], + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenRelated +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read'])] + public ?int $id = null; + + #[Groups(['golden:read', 'golden:embed', 'golden:noid'])] + public string $label = ''; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenRelationResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenRelationResource.php new file mode 100644 index 00000000000..6f41bcd2541 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenRelationResource.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use Symfony\Component\Serializer\Attribute\Groups; + +#[ApiResource( + shortName: 'GoldenRelation', + operations: [ + new Get( + uriTemplate: '/golden_relations/{id}', + name: 'golden_relation_get', + normalizationContext: ['groups' => ['golden:read', 'golden:embed']], + provider: [self::class, 'provide'], + ), + new Get( + uriTemplate: '/golden_relations_noid/{id}', + name: 'golden_relation_get_noid', + normalizationContext: ['groups' => ['golden:noid']], + provider: [self::class, 'provide'], + ), + ], +)] +final class GoldenRelationResource +{ + #[ApiProperty(identifier: true)] + #[Groups(['golden:read', 'golden:noid'])] + public ?int $id = null; + + #[ApiProperty(readableLink: false)] + #[Groups(['golden:read'])] + public ?GoldenRelated $linked = null; + + #[ApiProperty(readableLink: true)] + #[Groups(['golden:embed'])] + public ?GoldenRelated $embedded = null; + + #[ApiProperty(readableLink: true, genId: false)] + #[Groups(['golden:noid'])] + public ?GoldenRelated $embeddedWithoutId = null; + + public static function provide(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenStrictInputResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenStrictInputResource.php new file mode 100644 index 00000000000..3e5b217e76f --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenStrictInputResource.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Post; + +#[ApiResource( + shortName: 'GoldenStrictInput', + operations: [ + new Post( + uriTemplate: '/golden_strict_inputs', + name: 'golden_strict_input_post', + denormalizationContext: ['allow_extra_attributes' => false], + processor: [self::class, 'process'], + ), + ], +)] +final class GoldenStrictInputResource +{ + public string $name = ''; + + public ?int $quantity = null; + + public static function process(): null + { + return null; + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenTimestampable.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenTimestampable.php new file mode 100644 index 00000000000..69e124850eb --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenTimestampable.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +interface GoldenTimestampable +{ +} diff --git a/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenValidatedResource.php b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenValidatedResource.php new file mode 100644 index 00000000000..03e2ba79ee6 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/JsonSchemaGolden/GoldenValidatedResource.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource( + shortName: 'GoldenValidated', + operations: [ + new Post( + uriTemplate: '/golden_validated', + name: 'golden_validated_post', + denormalizationContext: ['groups' => ['golden:write']], + validationContext: ['groups' => ['golden:create']], + processor: [self::class, 'process'], + ), + ], +)] +final class GoldenValidatedResource +{ + #[Assert\NotBlank(groups: ['golden:create'])] + #[Assert\Length(max: 20, groups: ['golden:create'])] + #[Groups(['golden:write'])] + public string $name = ''; + + #[Assert\NotBlank(groups: ['golden:other'])] + #[Assert\Length(min: 3, groups: ['golden:other'])] + #[Groups(['golden:write'])] + public string $reference = ''; + + public static function process(): null + { + return null; + } +} diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index e670b1c9de8..1a3c7ea88e9 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -262,6 +262,9 @@ class_exists(NativePasswordHasher::class) ? 'password_hashers' : 'encoders' => [ 'max_query_depth' => 200, ], 'use_symfony_listeners' => $useSymfonyListeners, + 'json_schema' => [ + 'generator' => (bool) ($_SERVER['JSON_SCHEMA_GENERATOR'] ?? false), + ], 'defaults' => [ 'pagination_client_enabled' => true, 'pagination_client_items_per_page' => true, diff --git a/tests/Functional/JsonSchema/GoldenSchemaTest.php b/tests/Functional/JsonSchema/GoldenSchemaTest.php new file mode 100644 index 00000000000..a1fbc85e108 --- /dev/null +++ b/tests/Functional/JsonSchema/GoldenSchemaTest.php @@ -0,0 +1,563 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional\JsonSchema; + +use ApiPlatform\Hal\JsonSchema\SchemaFactory as HalSchemaFactory; +use ApiPlatform\Hydra\JsonSchema\ItemDefinitionProcessor; +use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory; +use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory; +use ApiPlatform\JsonSchema\Generator\ApiPlatformDefinitionPolicy; +use ApiPlatform\JsonSchema\Generator\ConfigurationFactory; +use ApiPlatform\JsonSchema\Generator\GeneratorSchemaFactory; +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactory; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Mcp\JsonSchema\SchemaFactory as McpSchemaFactory; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\AttributeSelectedAuthor; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\AttributeSelectedBook; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\BuiltinTypeQuirks; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenBook; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenDescribedOwner; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenDescribedRelated; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenGroupedItem; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenInputResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenIntersectionResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenMultiResourceEntity; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenMultiResourceOwner; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenNestedResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenNonStandardPutChild; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenOutputDtoOwner; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenOutputDtoResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenPartialChildrenOwner; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenPatchOnlyChild; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenPlainObject; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenQueryResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenRelated; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenRelationResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenStrictInputResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\JsonSchemaGolden\GoldenValidatedResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\McpFormatTool; +use ApiPlatform\Tests\Fixtures\TestBundle\Dto\McpBookOutputDto; +use ApiPlatform\Tests\Fixtures\TestBundle\Dto\SearchDto; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\McpBook; +use ApiPlatform\Tests\SetupClassResourcesTrait; +use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Component\JsonSchema\SchemaGenerator; + +class GoldenSchemaTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + private SchemaFactoryInterface $schemaFactory; + private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory; + + protected function setUp(): void + { + parent::setUp(); + $this->schemaFactory = self::getContainer()->get('api_platform.json_schema.schema_factory'); + $this->resourceMetadataCollectionFactory = self::getContainer()->get('api_platform.metadata.resource.metadata_collection_factory'); + } + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ + AttributeSelectedAuthor::class, + AttributeSelectedBook::class, + BuiltinTypeQuirks::class, + GoldenBook::class, + GoldenDescribedOwner::class, + GoldenDescribedRelated::class, + GoldenGroupedItem::class, + GoldenInputResource::class, + GoldenIntersectionResource::class, + GoldenMultiResourceEntity::class, + GoldenMultiResourceOwner::class, + GoldenNestedResource::class, + GoldenNonStandardPutChild::class, + GoldenOutputDtoOwner::class, + GoldenOutputDtoResource::class, + GoldenPartialChildrenOwner::class, + GoldenPatchOnlyChild::class, + GoldenQueryResource::class, + GoldenRelated::class, + GoldenRelationResource::class, + GoldenStrictInputResource::class, + GoldenValidatedResource::class, + McpBook::class, + McpFormatTool::class, + ]; + } + + /** + * @param array|null $serializerContext + * @param array $configuration + */ + #[DataProvider('provideCases')] + public function testBuildSchemaMatchesGolden(string $case, string $className, string $format, string $type, ?string $operationName, ?array $serializerContext, string $version, array $configuration): void + { + if ('output_intersection_type' === $case) { + $this->markTestSkipped('The old path emits anyOf for intersection types, fixed on 5.0 by fix/json-schema-intersection-allof.'); + } + + $this->assertNotEmpty($configuration); + + $operation = null; + if ('mcp' === ($configuration['schemaFactory'] ?? null)) { + $operation = $this->resourceMetadataCollectionFactory->create($configuration['mcpResource'] ?? $className)[0]->getMcp()[$operationName]; + } elseif (null !== $operationName) { + $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation($operationName); + } + + $factory = 'mcp' === ($configuration['schemaFactory'] ?? null) + ? self::getContainer()->get('api_platform.mcp.json_schema.schema_factory') + : $this->schemaFactory; + + $schema = $factory->buildSchema($className, $format, $type, $operation, new Schema($version), $serializerContext); + + $actual = json_encode(self::normalize([ + 'schema' => $schema->getArrayCopy(false), + 'definitions' => $schema->getDefinitions(), + ]), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR)."\n"; + + $file = __DIR__.'/golden/'.$case.'.json'; + if ('1' === getenv('UPDATE_GOLDEN')) { + file_put_contents($file, $actual); + $this->markTestIncomplete(\sprintf('Golden "%s" written.', $case)); + } + + $this->assertJsonStringEqualsJsonFile($file, $actual); + } + + /** + * @param array|null $serializerContext + * @param array $configuration + */ + #[DataProvider('provideCases')] + public function testGenerateMatchesGolden(string $case, string $className, string $format, string $type, ?string $operationName, ?array $serializerContext, string $version, array $configuration): void + { + if (!class_exists(SchemaGenerator::class)) { + $this->markTestSkipped('symfony/json-schema is not installed.'); + } + + if ('jsonld_nested_output_dto' === $case) { + $this->markTestSkipped('The Hydra decorator re-resolves the nested operation by format, so jsonld describes the output DTO of an embedded resource while json does not: pending decision.'); + } + + if ('groups_explicit_context_split' === $case) { + $this->markTestSkipped('Pins the explicit-context groups bug fixed in api-platform/core#8643.'); + } + + $isMcp = 'mcp' === ($configuration['schemaFactory'] ?? null); + $operation = null; + if ($isMcp) { + $operation = $this->resourceMetadataCollectionFactory->create($configuration['mcpResource'] ?? $className)[0]->getMcp()[$operationName]; + } elseif (null !== $operationName) { + $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation($operationName); + } + + $container = self::getContainer(); + $metadataFactory = $container->get('api_platform.metadata.resource.metadata_collection_factory'); + $resourceClassResolver = $container->get('api_platform.resource_class_resolver'); + $propertyMetadataFactory = $container->get('api_platform.metadata.property.metadata_factory'); + $policy = new ApiPlatformDefinitionPolicy($metadataFactory, $resourceClassResolver, $propertyMetadataFactory); + $factory = new GeneratorSchemaFactory( + new ConfigurationFactory($metadataFactory, $resourceClassResolver, $policy), + $policy, + $container->get('api_platform.metadata.property.name_collection_factory'), + $propertyMetadataFactory, + $resourceClassResolver, + $container->has('api_platform.name_converter') ? $container->get('api_platform.name_converter') : null, + [new ItemDefinitionProcessor($metadataFactory, $policy)], + ); + $definitionNameFactory = $container->get('api_platform.json_schema.definition_name_factory'); + $factory = new JsonApiSchemaFactory($factory, $propertyMetadataFactory, $resourceClassResolver, $metadataFactory, $definitionNameFactory, $container->get('api_platform.jsonapi.resource_linkage_resolver')); + $factory = new HydraSchemaFactory($factory, $container->getParameter('api_platform.serializer.default_context'), $definitionNameFactory, $metadataFactory); + $factory = new HalSchemaFactory($factory, $definitionNameFactory, $metadataFactory); + if ($isMcp) { + $factory = new McpSchemaFactory($factory); + } + + $schema = $factory->buildSchema($className, $format, $type, $operation, new Schema($version), $serializerContext); + + $actual = json_encode(self::normalize([ + 'schema' => $schema->getArrayCopy(false), + 'definitions' => $schema->getDefinitions(), + ]), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR)."\n"; + + $this->assertJsonStringEqualsJsonFile(__DIR__.'/golden/'.$case.'.json', $actual); + } + + /** + * @return iterable, string, array}> + */ + public static function provideCases(): iterable + { + $openApiGroups = ['groups' => ['golden:read']]; + + yield 'output_item' => ['output_item', GoldenBook::class, 'json', Schema::TYPE_OUTPUT, 'golden_book_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:read'], + 'description' => 'Retrieves a golden book.', + 'deprecated' => true, + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'output_collection' => ['output_collection', GoldenBook::class, 'json', Schema::TYPE_OUTPUT, 'golden_book_get_collection', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:read'], + 'collection' => true, + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'input_post_groups' => ['input_post_groups', GoldenInputResource::class, 'json', Schema::TYPE_INPUT, 'golden_input_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:write'], + 'namePrefix' => 'GoldenInput', + ]]; + + yield 'input_patch_merge' => ['input_patch_merge', GoldenInputResource::class, 'json', Schema::TYPE_INPUT, 'golden_input_patch', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:write'], + 'namePrefix' => 'GoldenInput', + ]]; + + yield 'input_put_nonstandard' => ['input_put_nonstandard', GoldenInputResource::class, 'json', Schema::TYPE_INPUT, 'golden_input_put_nonstandard', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:write'], + 'namePrefix' => 'GoldenInput', + ]]; + + yield 'input_validation_groups' => ['input_validation_groups', GoldenValidatedResource::class, 'json', Schema::TYPE_INPUT, 'golden_validated_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:write'], + 'validationGroups' => ['golden:create'], + 'namePrefix' => 'GoldenValidated', + ]]; + + yield 'output_nested_attributes' => ['output_nested_attributes', GoldenNestedResource::class, 'json', Schema::TYPE_OUTPUT, 'golden_nested_get_attributes', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'attributes' => ['name', 'author' => ['name']], + 'namePrefix' => 'GoldenNested', + ]]; + + yield 'output_attribute_selected_relation' => ['output_attribute_selected_relation', AttributeSelectedBook::class, 'json', Schema::TYPE_OUTPUT, 'attribute_selected_book_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'attributes' => ['title', 'author' => ['name']], + 'namePrefix' => 'AttributeSelectedBook', + ]]; + + yield 'input_attribute_selected_relation' => ['input_attribute_selected_relation', AttributeSelectedBook::class, 'json', Schema::TYPE_INPUT, 'attribute_selected_book_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'attributes' => ['title', 'author' => ['name']], + 'namePrefix' => 'AttributeSelectedBook', + ]]; + + yield 'output_ignored_attributes' => ['output_ignored_attributes', GoldenNestedResource::class, 'json', Schema::TYPE_OUTPUT, 'golden_nested_get_ignored', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'ignoredAttributes' => ['secret'], + 'namePrefix' => 'GoldenNested', + ]]; + + yield 'input_no_extra_attributes' => ['input_no_extra_attributes', GoldenStrictInputResource::class, 'json', Schema::TYPE_INPUT, 'golden_strict_input_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'allowExtraAttributes' => false, + 'namePrefix' => 'GoldenStrictInput', + ]]; + + yield 'output_definition_name' => ['output_definition_name', GoldenBook::class, 'json', Schema::TYPE_OUTPUT, 'golden_book_get', $openApiGroups + [SchemaFactory::OPENAPI_DEFINITION_NAME => 'Custom'], Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:read'], + 'definitionName' => 'Custom', + 'description' => 'Retrieves a golden book.', + 'deprecated' => true, + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'output_dto' => ['output_dto', GoldenOutputDtoResource::class, 'json', Schema::TYPE_OUTPUT, 'golden_output_dto_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'namePrefix' => 'GoldenOutputDtoResource', + ]]; + + yield 'output_relation_iri_vs_embed' => ['output_relation_iri_vs_embed', GoldenRelationResource::class, 'json', Schema::TYPE_OUTPUT, 'golden_relation_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:read', 'golden:embed'], + 'namePrefix' => 'GoldenRelation', + ]]; + + yield 'output_gen_id_false' => ['output_gen_id_false', GoldenRelationResource::class, 'json', Schema::TYPE_OUTPUT, 'golden_relation_get_noid', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:noid'], + 'namePrefix' => 'GoldenRelation', + ]]; + + yield 'non_resource_class' => ['non_resource_class', GoldenPlainObject::class, 'json', Schema::TYPE_OUTPUT, null, null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + ]]; + + yield 'input_query' => ['input_query', GoldenQueryResource::class, 'json', Schema::TYPE_INPUT, 'golden_query', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'namePrefix' => 'GoldenQuery', + ]]; + + yield 'dialect_openapi' => ['dialect_openapi', GoldenBook::class, 'json', Schema::TYPE_OUTPUT, 'golden_book_get', null, Schema::VERSION_OPENAPI, [ + 'dialect' => 'openApi31', + 'format' => 'json', + 'groups' => ['golden:read'], + 'description' => 'Retrieves a golden book.', + 'deprecated' => true, + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'dialect_swagger' => ['dialect_swagger', GoldenBook::class, 'json', Schema::TYPE_OUTPUT, 'golden_book_get', null, Schema::VERSION_SWAGGER, [ + 'dialect' => 'swagger20', + 'format' => 'json', + 'groups' => ['golden:read'], + 'description' => 'Retrieves a golden book.', + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'groups_normal' => ['groups_normal', GoldenGroupedItem::class, 'json', Schema::TYPE_OUTPUT, 'golden_grouped_item_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:read'], + 'namePrefix' => 'GoldenGroupedItem', + ]]; + + yield 'groups_explicit_context_split' => ['groups_explicit_context_split', GoldenGroupedItem::class, 'json', Schema::TYPE_OUTPUT, 'golden_grouped_item_get', ['foo' => 'bar'], Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'readGroups' => ['golden:read'], + 'writeGroups' => ['golden:write'], + 'namePrefix' => 'GoldenGroupedItem', + ]]; + + yield 'jsonld_output_item' => ['jsonld_output_item', GoldenBook::class, 'jsonld', Schema::TYPE_OUTPUT, 'golden_book_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'groups' => ['golden:read'], + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'jsonld_output_collection' => ['jsonld_output_collection', GoldenBook::class, 'jsonld', Schema::TYPE_OUTPUT, 'golden_book_get_collection', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'groups' => ['golden:read'], + 'collection' => true, + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'jsonld_input_post' => ['jsonld_input_post', GoldenInputResource::class, 'jsonld', Schema::TYPE_INPUT, 'golden_input_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'groups' => ['golden:write'], + 'namePrefix' => 'GoldenInput', + ]]; + + yield 'jsonld_relation_iri_vs_embed' => ['jsonld_relation_iri_vs_embed', GoldenRelationResource::class, 'jsonld', Schema::TYPE_OUTPUT, 'golden_relation_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'groups' => ['golden:read', 'golden:embed'], + 'namePrefix' => 'GoldenRelation', + ]]; + + yield 'jsonld_gen_id_false' => ['jsonld_gen_id_false', GoldenRelationResource::class, 'jsonld', Schema::TYPE_OUTPUT, 'golden_relation_get_noid', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'groups' => ['golden:noid'], + 'namePrefix' => 'GoldenRelation', + ]]; + + yield 'jsonhal_output_item' => ['jsonhal_output_item', GoldenBook::class, 'jsonhal', Schema::TYPE_OUTPUT, 'golden_book_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonhal', + 'groups' => ['golden:read'], + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'jsonhal_relation_iri_vs_embed' => ['jsonhal_relation_iri_vs_embed', GoldenRelationResource::class, 'jsonhal', Schema::TYPE_OUTPUT, 'golden_relation_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonhal', + 'groups' => ['golden:read', 'golden:embed'], + 'namePrefix' => 'GoldenRelation', + ]]; + + yield 'jsonhal_output_collection' => ['jsonhal_output_collection', GoldenBook::class, 'jsonhal', Schema::TYPE_OUTPUT, 'golden_book_get_collection', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonhal', + 'groups' => ['golden:read'], + 'collection' => true, + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'jsonapi_output_item' => ['jsonapi_output_item', GoldenBook::class, 'jsonapi', Schema::TYPE_OUTPUT, 'golden_book_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonapi', + 'groups' => ['golden:read'], + 'namePrefix' => 'GoldenBook', + ]]; + + yield 'jsonapi_relation_iri_vs_embed' => ['jsonapi_relation_iri_vs_embed', GoldenRelationResource::class, 'jsonapi', Schema::TYPE_OUTPUT, 'golden_relation_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonapi', + 'groups' => ['golden:read', 'golden:embed'], + 'namePrefix' => 'GoldenRelation', + ]]; + + yield 'jsonapi_input_post' => ['jsonapi_input_post', GoldenInputResource::class, 'jsonapi', Schema::TYPE_INPUT, 'golden_input_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonapi', + 'groups' => ['golden:write'], + 'namePrefix' => 'GoldenInput', + ]]; + + yield 'multi_resource_nested_output' => ['multi_resource_nested_output', GoldenMultiResourceOwner::class, 'json', Schema::TYPE_OUTPUT, 'golden_multi_resource_owner_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:read'], + 'namePrefix' => 'GoldenMultiResourceOwner', + ]]; + + yield 'multi_resource_nested_input' => ['multi_resource_nested_input', GoldenMultiResourceOwner::class, 'json', Schema::TYPE_INPUT, 'golden_multi_resource_owner_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:write'], + 'namePrefix' => 'GoldenMultiResourceOwner', + ]]; + + yield 'input_nested_partial_children' => ['input_nested_partial_children', GoldenPartialChildrenOwner::class, 'json', Schema::TYPE_INPUT, 'golden_partial_children_owner_post', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'groups' => ['golden:write'], + 'namePrefix' => 'GoldenPartialChildrenOwner', + ]]; + + yield 'output_builtin_type_quirks' => ['output_builtin_type_quirks', BuiltinTypeQuirks::class, 'json', Schema::TYPE_OUTPUT, 'builtin_type_quirks_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'namePrefix' => 'BuiltinTypeQuirks', + ]]; + + yield 'output_intersection_type' => ['output_intersection_type', GoldenIntersectionResource::class, 'json', Schema::TYPE_OUTPUT, 'golden_intersection_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'namePrefix' => 'GoldenIntersection', + ]]; + + yield 'nested_described_resource' => ['nested_described_resource', GoldenDescribedOwner::class, 'json', Schema::TYPE_OUTPUT, 'golden_described_owner_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'namePrefix' => 'GoldenDescribedOwner', + ]]; + + yield 'nested_output_dto' => ['nested_output_dto', GoldenOutputDtoOwner::class, 'json', Schema::TYPE_OUTPUT, 'golden_output_dto_owner_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'namePrefix' => 'GoldenOutputDtoOwner', + ]]; + + yield 'jsonld_nested_described_resource' => ['jsonld_nested_described_resource', GoldenDescribedOwner::class, 'jsonld', Schema::TYPE_OUTPUT, 'golden_described_owner_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'namePrefix' => 'GoldenDescribedOwner', + ]]; + + yield 'jsonld_nested_output_dto' => ['jsonld_nested_output_dto', GoldenOutputDtoOwner::class, 'jsonld', Schema::TYPE_OUTPUT, 'golden_output_dto_owner_get', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'namePrefix' => 'GoldenOutputDtoOwner', + ]]; + + yield 'mcp_tool_output' => ['mcp_tool_output', McpFormatTool::class, 'json', Schema::TYPE_OUTPUT, 'format_message', null, Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'schemaFactory' => 'mcp', + 'namePrefix' => 'McpFormatTool', + ]]; + + yield 'mcp_loader_item_output' => ['mcp_loader_item_output', McpBook::class, 'jsonld', Schema::TYPE_OUTPUT, 'get_book_info', [SchemaFactory::FORCE_SUBSCHEMA => true], Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'schemaFactory' => 'mcp', + 'namePrefix' => 'McpBook', + ]]; + + yield 'mcp_loader_collection_output' => ['mcp_loader_collection_output', McpBook::class, 'jsonld', Schema::TYPE_OUTPUT, 'list_books', [SchemaFactory::FORCE_SUBSCHEMA => true], Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'schemaFactory' => 'mcp', + 'collection' => true, + 'namePrefix' => 'McpBook', + ]]; + + yield 'mcp_loader_output_dto' => ['mcp_loader_output_dto', McpBookOutputDto::class, 'jsonld', Schema::TYPE_OUTPUT, 'list_books_dto', [SchemaFactory::FORCE_SUBSCHEMA => true], Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'jsonld', + 'schemaFactory' => 'mcp', + 'mcpResource' => McpBook::class, + 'namePrefix' => 'McpBook', + ]]; + + yield 'mcp_loader_input' => ['mcp_loader_input', SearchDto::class, 'jsonld', Schema::TYPE_INPUT, 'list_books', [SchemaFactory::FORCE_SUBSCHEMA => true], Schema::VERSION_JSON_SCHEMA, [ + 'dialect' => 'jsonSchema202012', + 'format' => 'json', + 'schemaFactory' => 'mcp', + 'mcpResource' => McpBook::class, + ]]; + } + + private static function normalize(mixed $value): mixed + { + $value = json_decode(json_encode($value, \JSON_THROW_ON_ERROR), true, 512, \JSON_THROW_ON_ERROR); + + return self::sortKeys($value); + } + + private static function sortKeys(mixed $value): mixed + { + if (!\is_array($value)) { + return $value; + } + + $value = array_map(self::sortKeys(...), $value); + if (!array_is_list($value)) { + ksort($value); + } + + return $value; + } +} diff --git a/tests/Functional/JsonSchema/golden/dialect_openapi.json b/tests/Functional/JsonSchema/golden/dialect_openapi.json new file mode 100644 index 00000000000..1b716f25808 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/dialect_openapi.json @@ -0,0 +1,36 @@ +{ + "definitions": { + "GoldenBook-golden.read": { + "deprecated": true, + "description": "Retrieves a golden book.", + "externalDocs": { + "url": "https://schema.org/Book" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/components/schemas/GoldenBook-golden.read" + } +} diff --git a/tests/Functional/JsonSchema/golden/dialect_swagger.json b/tests/Functional/JsonSchema/golden/dialect_swagger.json new file mode 100644 index 00000000000..5936fd36783 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/dialect_swagger.json @@ -0,0 +1,35 @@ +{ + "definitions": { + "GoldenBook-golden.read": { + "description": "Retrieves a golden book.", + "externalDocs": { + "url": "https://schema.org/Book" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenBook-golden.read" + } +} diff --git a/tests/Functional/JsonSchema/golden/groups_explicit_context_split.json b/tests/Functional/JsonSchema/golden/groups_explicit_context_split.json new file mode 100644 index 00000000000..8711fcf6573 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/groups_explicit_context_split.json @@ -0,0 +1,38 @@ +{ + "definitions": { + "GoldenGroupedItem": { + "properties": { + "createdAt": { + "readOnly": true, + "type": [ + "string", + "null" + ] + }, + "id": { + "readOnly": true, + "type": [ + "integer", + "null" + ] + }, + "secretToken": { + "type": [ + "string", + "null" + ], + "writeOnly": true + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenGroupedItem", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/groups_normal.json b/tests/Functional/JsonSchema/golden/groups_normal.json new file mode 100644 index 00000000000..afa4e4a3a3a --- /dev/null +++ b/tests/Functional/JsonSchema/golden/groups_normal.json @@ -0,0 +1,29 @@ +{ + "definitions": { + "GoldenGroupedItem-golden.read": { + "properties": { + "createdAt": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenGroupedItem-golden.read", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_attribute_selected_relation.json b/tests/Functional/JsonSchema/golden/input_attribute_selected_relation.json new file mode 100644 index 00000000000..e3ef1fbafc4 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_attribute_selected_relation.json @@ -0,0 +1,36 @@ +{ + "definitions": { + "AttributeSelectedAuthor-name": { + "properties": { + "name": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "AttributeSelectedBook-title_author.name": { + "properties": { + "author": { + "anyOf": [ + { + "$ref": "#/definitions/AttributeSelectedAuthor-name" + }, + { + "type": "null" + } + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/AttributeSelectedBook-title_author.name", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_nested_partial_children.json b/tests/Functional/JsonSchema/golden/input_nested_partial_children.json new file mode 100644 index 00000000000..80fd155c77e --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_nested_partial_children.json @@ -0,0 +1,58 @@ +{ + "definitions": { + "GoldenNonStandardPutChild-golden.write.partial": { + "properties": { + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenPartialChildrenOwner-golden.write": { + "properties": { + "name": { + "default": "", + "type": "string" + }, + "patchChild": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenPatchOnlyChild-golden.write.jsonMergePatch" + }, + { + "type": "null" + } + ] + }, + "putChild": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenNonStandardPutChild-golden.write.partial" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "GoldenPatchOnlyChild-golden.write.jsonMergePatch": { + "properties": { + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenPartialChildrenOwner-golden.write", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_no_extra_attributes.json b/tests/Functional/JsonSchema/golden/input_no_extra_attributes.json new file mode 100644 index 00000000000..48f98c6aa7e --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_no_extra_attributes.json @@ -0,0 +1,24 @@ +{ + "definitions": { + "GoldenStrictInput": { + "additionalProperties": false, + "properties": { + "name": { + "default": "", + "type": "string" + }, + "quantity": { + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenStrictInput", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_patch_merge.json b/tests/Functional/JsonSchema/golden/input_patch_merge.json new file mode 100644 index 00000000000..e3d4efb13ed --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_patch_merge.json @@ -0,0 +1,31 @@ +{ + "definitions": { + "GoldenInput-golden.write.jsonMergePatch": { + "properties": { + "computedLabel": { + "readOnly": true, + "type": [ + "string", + "null" + ] + }, + "password": { + "type": [ + "string", + "null" + ], + "writeOnly": true + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenInput-golden.write.jsonMergePatch", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_post_groups.json b/tests/Functional/JsonSchema/golden/input_post_groups.json new file mode 100644 index 00000000000..47b51ccbda0 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_post_groups.json @@ -0,0 +1,34 @@ +{ + "definitions": { + "GoldenInput-golden.write": { + "properties": { + "computedLabel": { + "readOnly": true, + "type": [ + "string", + "null" + ] + }, + "password": { + "type": [ + "string", + "null" + ], + "writeOnly": true + }, + "title": { + "default": "", + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenInput-golden.write", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_put_nonstandard.json b/tests/Functional/JsonSchema/golden/input_put_nonstandard.json new file mode 100644 index 00000000000..ce3de394773 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_put_nonstandard.json @@ -0,0 +1,31 @@ +{ + "definitions": { + "GoldenInput-golden.write.partial": { + "properties": { + "computedLabel": { + "readOnly": true, + "type": [ + "string", + "null" + ] + }, + "password": { + "type": [ + "string", + "null" + ], + "writeOnly": true + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenInput-golden.write.partial", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_query.json b/tests/Functional/JsonSchema/golden/input_query.json new file mode 100644 index 00000000000..3ed58370002 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_query.json @@ -0,0 +1,26 @@ +{ + "definitions": { + "GoldenQuery.GoldenQueryCriteria": { + "properties": { + "limit": { + "type": [ + "integer", + "null" + ] + }, + "term": { + "default": "", + "type": "string" + } + }, + "required": [ + "term" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenQuery.GoldenQueryCriteria", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/input_validation_groups.json b/tests/Functional/JsonSchema/golden/input_validation_groups.json new file mode 100644 index 00000000000..4c641a538a4 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/input_validation_groups.json @@ -0,0 +1,25 @@ +{ + "definitions": { + "GoldenValidated-golden.write_validation.golden.create": { + "properties": { + "name": { + "default": "", + "maxLength": 20, + "type": "string" + }, + "reference": { + "default": "", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenValidated-golden.write_validation.golden.create", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonapi_input_post.json b/tests/Functional/JsonSchema/golden/jsonapi_input_post.json new file mode 100644 index 00000000000..47b51ccbda0 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonapi_input_post.json @@ -0,0 +1,34 @@ +{ + "definitions": { + "GoldenInput-golden.write": { + "properties": { + "computedLabel": { + "readOnly": true, + "type": [ + "string", + "null" + ] + }, + "password": { + "type": [ + "string", + "null" + ], + "writeOnly": true + }, + "title": { + "default": "", + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenInput-golden.write", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonapi_output_item.json b/tests/Functional/JsonSchema/golden/jsonapi_output_item.json new file mode 100644 index 00000000000..fba80f02377 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonapi_output_item.json @@ -0,0 +1,88 @@ +{ + "definitions": { + "GoldenBook": { + "deprecated": true, + "description": "Retrieves a golden book.", + "externalDocs": { + "url": "https://schema.org/Book" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "internalNote": { + "default": "", + "type": "string" + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenBook.jsonapi": { + "properties": { + "data": { + "properties": { + "attributes": { + "properties": { + "_id": { + "type": [ + "integer", + "null" + ] + }, + "internalNote": { + "default": "", + "type": "string" + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + } + } + } + }, + "schema": { + "$ref": "#/definitions/GoldenBook.jsonapi", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonapi_relation_iri_vs_embed.json b/tests/Functional/JsonSchema/golden/jsonapi_relation_iri_vs_embed.json new file mode 100644 index 00000000000..3604a511dc5 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonapi_relation_iri_vs_embed.json @@ -0,0 +1,242 @@ +{ + "definitions": { + "GoldenRelated": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenRelated.jsonapi": { + "properties": { + "data": { + "properties": { + "attributes": { + "properties": { + "_id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + } + } + }, + "GoldenRelated_noid": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenRelation": { + "properties": { + "embedded": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated" + }, + { + "type": "null" + } + ] + }, + "embeddedWithoutId": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated_noid" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + }, + "linked": { + "example": "https://example.com/", + "format": "iri-reference", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "GoldenRelation.jsonapi": { + "properties": { + "data": { + "properties": { + "attributes": { + "properties": { + "_id": { + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "id": { + "type": "string" + }, + "relationships": { + "properties": { + "embedded": { + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "properties": { + "id": { + "format": "iri-reference", + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + } + ] + } + } + }, + "embeddedWithoutId": { + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "properties": { + "id": { + "format": "iri-reference", + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + } + ] + } + } + }, + "linked": { + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "properties": { + "id": { + "format": "iri-reference", + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + } + ] + } + } + } + }, + "type": "object" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ], + "type": "object" + }, + "included": { + "description": "Related resources requested via the \"include\" query parameter.", + "externalDocs": { + "url": "https://jsonapi.org/format/#fetching-includes" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated.jsonapi" + } + ] + }, + "readOnly": true, + "type": "array" + } + } + } + }, + "schema": { + "$ref": "#/definitions/GoldenRelation.jsonapi", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonhal_output_collection.json b/tests/Functional/JsonSchema/golden/jsonhal_output_collection.json new file mode 100644 index 00000000000..b3cca4c61ad --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonhal_output_collection.json @@ -0,0 +1,174 @@ +{ + "definitions": { + "GoldenBook-golden.read": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenBook.jsonhal-golden.read": { + "allOf": [ + { + "properties": { + "_links": { + "properties": { + "self": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + { + "$ref": "#/definitions/GoldenBook-golden.read" + } + ] + }, + "HalCollectionBaseSchema": { + "allOf": [ + { + "$ref": "#/definitions/HalCollectionBaseSchemaNoPagination" + }, + { + "properties": { + "_links": { + "properties": { + "first": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + }, + "last": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + }, + "next": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + }, + "previous": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + } + } + }, + "itemsPerPage": { + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + } + ] + }, + "HalCollectionBaseSchemaNoPagination": { + "properties": { + "_embedded": { + "anyOf": [ + { + "properties": { + "item": { + "type": "array" + } + }, + "type": "object" + }, + { + "type": "object" + } + ] + }, + "_links": { + "properties": { + "self": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "totalItems": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "_links", + "_embedded" + ], + "type": "object" + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { + "$ref": "#/definitions/HalCollectionBaseSchema" + }, + { + "properties": { + "_embedded": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/GoldenBook.jsonhal-golden.read" + }, + "type": "array" + } + } + }, + "type": "object" + } + ], + "description": "GoldenBook.jsonhal-golden.read collection." + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonhal_output_item.json b/tests/Functional/JsonSchema/golden/jsonhal_output_item.json new file mode 100644 index 00000000000..01482eaa35a --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonhal_output_item.json @@ -0,0 +1,64 @@ +{ + "definitions": { + "GoldenBook-golden.read": { + "deprecated": true, + "description": "Retrieves a golden book.", + "externalDocs": { + "url": "https://schema.org/Book" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenBook.jsonhal-golden.read": { + "allOf": [ + { + "properties": { + "_links": { + "properties": { + "self": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + { + "$ref": "#/definitions/GoldenBook-golden.read" + } + ], + "description": "Retrieves a golden book." + } + }, + "schema": { + "$ref": "#/definitions/GoldenBook.jsonhal-golden.read", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonhal_relation_iri_vs_embed.json b/tests/Functional/JsonSchema/golden/jsonhal_relation_iri_vs_embed.json new file mode 100644 index 00000000000..861340a9550 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonhal_relation_iri_vs_embed.json @@ -0,0 +1,78 @@ +{ + "definitions": { + "GoldenRelated-golden.read_golden.embed": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenRelation-golden.read_golden.embed": { + "properties": { + "embedded": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated-golden.read_golden.embed" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + }, + "linked": { + "example": "https://example.com/", + "format": "iri-reference", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "GoldenRelation.jsonhal-golden.read_golden.embed": { + "allOf": [ + { + "properties": { + "_links": { + "properties": { + "self": { + "properties": { + "href": { + "format": "iri-reference", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + }, + "type": "object" + }, + { + "$ref": "#/definitions/GoldenRelation-golden.read_golden.embed" + } + ] + } + }, + "schema": { + "$ref": "#/definitions/GoldenRelation.jsonhal-golden.read_golden.embed", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonld_gen_id_false.json b/tests/Functional/JsonSchema/golden/jsonld_gen_id_false.json new file mode 100644 index 00000000000..f3748202921 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonld_gen_id_false.json @@ -0,0 +1,132 @@ +{ + "definitions": { + "GoldenRelated.jsonld-golden.noid_noid": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchemaWithoutId" + }, + { + "properties": { + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + ] + }, + "GoldenRelation.jsonld-golden.noid": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "properties": { + "embeddedWithoutId": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated.jsonld-golden.noid_noid" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + ] + }, + "HydraItemBaseSchema": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + } + }, + "required": [ + "@id", + "@type" + ], + "type": "object" + }, + "HydraItemBaseSchemaWithoutId": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + } + }, + "required": [ + "@type" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenRelation.jsonld-golden.noid" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonld_input_post.json b/tests/Functional/JsonSchema/golden/jsonld_input_post.json new file mode 100644 index 00000000000..47b51ccbda0 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonld_input_post.json @@ -0,0 +1,34 @@ +{ + "definitions": { + "GoldenInput-golden.write": { + "properties": { + "computedLabel": { + "readOnly": true, + "type": [ + "string", + "null" + ] + }, + "password": { + "type": [ + "string", + "null" + ], + "writeOnly": true + }, + "title": { + "default": "", + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenInput-golden.write", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonld_nested_described_resource.json b/tests/Functional/JsonSchema/golden/jsonld_nested_described_resource.json new file mode 100644 index 00000000000..d20fb6eeaec --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonld_nested_described_resource.json @@ -0,0 +1,102 @@ +{ + "definitions": { + "GoldenDescribedAlias.jsonld": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "externalDocs": { + "url": "https://schema.org/Thing" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + ], + "description": "A described related resource." + }, + "GoldenDescribedOwner.jsonld": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "related": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenDescribedAlias.jsonld" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + } + ] + }, + "HydraItemBaseSchema": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + } + }, + "required": [ + "@id", + "@type" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenDescribedOwner.jsonld" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonld_nested_output_dto.json b/tests/Functional/JsonSchema/golden/jsonld_nested_output_dto.json new file mode 100644 index 00000000000..bc176e92714 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonld_nested_output_dto.json @@ -0,0 +1,96 @@ +{ + "definitions": { + "GoldenOutputDtoOwner.jsonld": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "properties": { + "dto": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenOutputDtoResource.GoldenOutputDto.jsonld" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + ] + }, + "GoldenOutputDtoResource.GoldenOutputDto.jsonld": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "properties": { + "label": { + "default": "", + "type": "string" + }, + "total": { + "default": 0, + "type": "integer" + } + }, + "type": "object" + } + ] + }, + "HydraItemBaseSchema": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + } + }, + "required": [ + "@id", + "@type" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenOutputDtoOwner.jsonld" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonld_output_collection.json b/tests/Functional/JsonSchema/golden/jsonld_output_collection.json new file mode 100644 index 00000000000..bf0809e0db8 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonld_output_collection.json @@ -0,0 +1,206 @@ +{ + "definitions": { + "GoldenBook.jsonld-golden.read": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + ] + }, + "HydraCollectionBaseSchema": { + "allOf": [ + { + "$ref": "#/definitions/HydraCollectionBaseSchemaNoPagination" + }, + { + "properties": { + "hydra:view": { + "example": { + "@id": "string", + "@type": "string", + "hydra:first": "string", + "hydra:last": "string", + "hydra:next": "string", + "hydra:previous": "string" + }, + "properties": { + "@id": { + "format": "iri-reference", + "type": "string" + }, + "@type": { + "type": "string" + }, + "hydra:first": { + "format": "iri-reference", + "type": [ + "string", + "null" + ] + }, + "hydra:last": { + "format": "iri-reference", + "type": [ + "string", + "null" + ] + }, + "hydra:next": { + "format": "iri-reference", + "type": [ + "string", + "null" + ] + }, + "hydra:previous": { + "format": "iri-reference", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + }, + "type": "object" + } + ] + }, + "HydraCollectionBaseSchemaNoPagination": { + "properties": { + "hydra:search": { + "properties": { + "@type": { + "type": "string" + }, + "hydra:mapping": { + "items": { + "properties": { + "@type": { + "type": "string" + }, + "property": { + "type": [ + "string", + "null" + ] + }, + "required": { + "type": "boolean" + }, + "variable": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "hydra:template": { + "type": "string" + }, + "hydra:variableRepresentation": { + "type": "string" + } + }, + "type": "object" + }, + "hydra:totalItems": { + "minimum": 0, + "type": "integer" + } + }, + "type": "object" + }, + "HydraItemBaseSchema": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + } + }, + "required": [ + "@id", + "@type" + ], + "type": "object" + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "allOf": [ + { + "$ref": "#/definitions/HydraCollectionBaseSchema" + }, + { + "properties": { + "hydra:member": { + "items": { + "$ref": "#/definitions/GoldenBook.jsonld-golden.read" + }, + "type": "array" + } + }, + "required": [ + "hydra:member" + ], + "type": "object" + } + ], + "description": "GoldenBook.jsonld-golden.read collection.", + "type": "object" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonld_output_item.json b/tests/Functional/JsonSchema/golden/jsonld_output_item.json new file mode 100644 index 00000000000..494ef927369 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonld_output_item.json @@ -0,0 +1,84 @@ +{ + "definitions": { + "GoldenBook.jsonld-golden.read": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "deprecated": true, + "externalDocs": { + "url": "https://schema.org/Book" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + ], + "description": "Retrieves a golden book." + }, + "HydraItemBaseSchema": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + } + }, + "required": [ + "@id", + "@type" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenBook.jsonld-golden.read" + } +} diff --git a/tests/Functional/JsonSchema/golden/jsonld_relation_iri_vs_embed.json b/tests/Functional/JsonSchema/golden/jsonld_relation_iri_vs_embed.json new file mode 100644 index 00000000000..f2dbf34528a --- /dev/null +++ b/tests/Functional/JsonSchema/golden/jsonld_relation_iri_vs_embed.json @@ -0,0 +1,106 @@ +{ + "definitions": { + "GoldenRelated.jsonld-golden.read_golden.embed": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + ] + }, + "GoldenRelation.jsonld-golden.read_golden.embed": { + "allOf": [ + { + "$ref": "#/definitions/HydraItemBaseSchema" + }, + { + "properties": { + "embedded": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated.jsonld-golden.read_golden.embed" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + }, + "linked": { + "example": "https://example.com/", + "format": "iri-reference", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + ] + }, + "HydraItemBaseSchema": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + } + }, + "required": [ + "@id", + "@type" + ], + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenRelation.jsonld-golden.read_golden.embed" + } +} diff --git a/tests/Functional/JsonSchema/golden/mcp_loader_collection_output.json b/tests/Functional/JsonSchema/golden/mcp_loader_collection_output.json new file mode 100644 index 00000000000..5a3d6ceb8ee --- /dev/null +++ b/tests/Functional/JsonSchema/golden/mcp_loader_collection_output.json @@ -0,0 +1,186 @@ +{ + "definitions": [], + "schema": { + "description": "McpBook.jsonld collection.", + "properties": { + "hydra:member": { + "items": { + "description": "List Books", + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + }, + "id": { + "readOnly": true, + "type": "integer" + }, + "isbn": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "@id", + "@type" + ], + "type": "object" + }, + "type": "array" + }, + "hydra:search": { + "properties": { + "@type": { + "type": "string" + }, + "hydra:mapping": { + "items": { + "properties": { + "@type": { + "type": "string" + }, + "property": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "required": { + "type": "boolean" + }, + "variable": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "hydra:template": { + "type": "string" + }, + "hydra:variableRepresentation": { + "type": "string" + } + }, + "type": "object" + }, + "hydra:totalItems": { + "minimum": 0, + "type": "integer" + }, + "hydra:view": { + "example": { + "@id": "string", + "@type": "string", + "hydra:first": "string", + "hydra:last": "string", + "hydra:next": "string", + "hydra:previous": "string" + }, + "properties": { + "@id": { + "format": "iri-reference", + "type": "string" + }, + "@type": { + "type": "string" + }, + "hydra:first": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "format": "iri-reference" + }, + "hydra:last": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "format": "iri-reference" + }, + "hydra:next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "format": "iri-reference" + }, + "hydra:previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "format": "iri-reference" + } + }, + "type": "object" + } + }, + "required": [ + "hydra:member" + ], + "type": "object" + } +} diff --git a/tests/Functional/JsonSchema/golden/mcp_loader_input.json b/tests/Functional/JsonSchema/golden/mcp_loader_input.json new file mode 100644 index 00000000000..3a6448d8758 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/mcp_loader_input.json @@ -0,0 +1,11 @@ +{ + "definitions": [], + "schema": { + "properties": { + "search": { + "type": "string" + } + }, + "type": "object" + } +} diff --git a/tests/Functional/JsonSchema/golden/mcp_loader_item_output.json b/tests/Functional/JsonSchema/golden/mcp_loader_item_output.json new file mode 100644 index 00000000000..b44e85adafb --- /dev/null +++ b/tests/Functional/JsonSchema/golden/mcp_loader_item_output.json @@ -0,0 +1,63 @@ +{ + "definitions": [], + "schema": { + "properties": { + "@context": { + "oneOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "properties": { + "@vocab": { + "type": "string" + }, + "hydra": { + "enum": [ + "http://www.w3.org/ns/hydra/core#" + ], + "type": "string" + } + }, + "required": [ + "@vocab", + "hydra" + ], + "type": "object" + } + ] + }, + "@id": { + "type": "string" + }, + "@type": { + "type": "string" + }, + "id": { + "readOnly": true, + "type": "integer" + }, + "isbn": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" + } + }, + "required": [ + "@type" + ], + "type": "object" + } +} diff --git a/tests/Functional/JsonSchema/golden/mcp_loader_output_dto.json b/tests/Functional/JsonSchema/golden/mcp_loader_output_dto.json new file mode 100644 index 00000000000..605a6be16ed --- /dev/null +++ b/tests/Functional/JsonSchema/golden/mcp_loader_output_dto.json @@ -0,0 +1,17 @@ +{ + "definitions": [], + "schema": { + "properties": { + "id": { + "type": "integer" + }, + "isbn": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + } +} diff --git a/tests/Functional/JsonSchema/golden/mcp_tool_output.json b/tests/Functional/JsonSchema/golden/mcp_tool_output.json new file mode 100644 index 00000000000..f05ae5b0ad2 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/mcp_tool_output.json @@ -0,0 +1,12 @@ +{ + "definitions": [], + "schema": { + "description": "Echo a message, serialized with the format declared on the operation", + "properties": { + "message": { + "type": "string" + } + }, + "type": "object" + } +} diff --git a/tests/Functional/JsonSchema/golden/multi_resource_nested_input.json b/tests/Functional/JsonSchema/golden/multi_resource_nested_input.json new file mode 100644 index 00000000000..d1c3e8490e4 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/multi_resource_nested_input.json @@ -0,0 +1,32 @@ +{ + "definitions": { + "GoldenMultiResource-golden.write": { + "properties": { + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenMultiResourceOwner-golden.write": { + "properties": { + "entity": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenMultiResource-golden.write" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenMultiResourceOwner-golden.write", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/multi_resource_nested_output.json b/tests/Functional/JsonSchema/golden/multi_resource_nested_output.json new file mode 100644 index 00000000000..9a1abe5a3ca --- /dev/null +++ b/tests/Functional/JsonSchema/golden/multi_resource_nested_output.json @@ -0,0 +1,44 @@ +{ + "definitions": { + "GoldenAdminMultiResource-golden.read": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenMultiResourceOwner-golden.read": { + "properties": { + "entity": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenAdminMultiResource-golden.read" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenMultiResourceOwner-golden.read", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/nested_described_resource.json b/tests/Functional/JsonSchema/golden/nested_described_resource.json new file mode 100644 index 00000000000..ddfde699441 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/nested_described_resource.json @@ -0,0 +1,48 @@ +{ + "definitions": { + "GoldenDescribedAlias": { + "description": "A described related resource.", + "externalDocs": { + "url": "https://schema.org/Thing" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenDescribedOwner": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "related": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenDescribedAlias" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenDescribedOwner", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/nested_output_dto.json b/tests/Functional/JsonSchema/golden/nested_output_dto.json new file mode 100644 index 00000000000..a7db24e9103 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/nested_output_dto.json @@ -0,0 +1,44 @@ +{ + "definitions": { + "GoldenOutputDtoOwner": { + "properties": { + "dto": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenOutputDtoResource" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "GoldenOutputDtoResource": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "internal": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenOutputDtoOwner", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/non_resource_class.json b/tests/Functional/JsonSchema/golden/non_resource_class.json new file mode 100644 index 00000000000..9c1d34c4eec --- /dev/null +++ b/tests/Functional/JsonSchema/golden/non_resource_class.json @@ -0,0 +1,27 @@ +{ + "definitions": { + "GoldenPlainObject": { + "properties": { + "count": { + "type": [ + "integer", + "null" + ] + }, + "flag": { + "default": false, + "type": "boolean" + }, + "name": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenPlainObject", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_attribute_selected_relation.json b/tests/Functional/JsonSchema/golden/output_attribute_selected_relation.json new file mode 100644 index 00000000000..e3ef1fbafc4 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_attribute_selected_relation.json @@ -0,0 +1,36 @@ +{ + "definitions": { + "AttributeSelectedAuthor-name": { + "properties": { + "name": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "AttributeSelectedBook-title_author.name": { + "properties": { + "author": { + "anyOf": [ + { + "$ref": "#/definitions/AttributeSelectedAuthor-name" + }, + { + "type": "null" + } + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/AttributeSelectedBook-title_author.name", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_builtin_type_quirks.json b/tests/Functional/JsonSchema/golden/output_builtin_type_quirks.json new file mode 100644 index 00000000000..482a3cdba42 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_builtin_type_quirks.json @@ -0,0 +1,31 @@ +{ + "definitions": { + "BuiltinTypeQuirks": { + "properties": { + "amount": { + "format": "string", + "type": [ + "string", + "null" + ] + }, + "anything": [], + "id": { + "type": [ + "integer", + "null" + ] + }, + "untyped": { + "items": [], + "type": "array" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/BuiltinTypeQuirks", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_collection.json b/tests/Functional/JsonSchema/golden/output_collection.json new file mode 100644 index 00000000000..46403d17639 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_collection.json @@ -0,0 +1,35 @@ +{ + "definitions": { + "GoldenBook-golden.read": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": { + "$ref": "#/definitions/GoldenBook-golden.read" + }, + "type": "array" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_definition_name.json b/tests/Functional/JsonSchema/golden/output_definition_name.json new file mode 100644 index 00000000000..256661fe617 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_definition_name.json @@ -0,0 +1,37 @@ +{ + "definitions": { + "GoldenBook-Custom": { + "deprecated": true, + "description": "Retrieves a golden book.", + "externalDocs": { + "url": "https://schema.org/Book" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenBook-Custom", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_dto.json b/tests/Functional/JsonSchema/golden/output_dto.json new file mode 100644 index 00000000000..b8eb10881c6 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_dto.json @@ -0,0 +1,21 @@ +{ + "definitions": { + "GoldenOutputDtoResource.GoldenOutputDto": { + "properties": { + "label": { + "default": "", + "type": "string" + }, + "total": { + "default": 0, + "type": "integer" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenOutputDtoResource.GoldenOutputDto", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_gen_id_false.json b/tests/Functional/JsonSchema/golden/output_gen_id_false.json new file mode 100644 index 00000000000..b3ecde2e6e2 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_gen_id_false.json @@ -0,0 +1,38 @@ +{ + "definitions": { + "GoldenRelated-golden.noid_noid": { + "properties": { + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenRelation-golden.noid": { + "properties": { + "embeddedWithoutId": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated-golden.noid_noid" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenRelation-golden.noid", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_ignored_attributes.json b/tests/Functional/JsonSchema/golden/output_ignored_attributes.json new file mode 100644 index 00000000000..0eb7e0dc47d --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_ignored_attributes.json @@ -0,0 +1,46 @@ +{ + "definitions": { + "GoldenAuthor": { + "properties": { + "email": { + "default": "", + "type": "string" + }, + "name": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenNested": { + "properties": { + "author": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenAuthor" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + }, + "name": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenNested", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_intersection_type.json b/tests/Functional/JsonSchema/golden/output_intersection_type.json new file mode 100644 index 00000000000..236a34ddce8 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_intersection_type.json @@ -0,0 +1,35 @@ +{ + "definitions": { + "GoldenActivable": { + "type": "object" + }, + "GoldenIntersection": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "library": { + "allOf": [ + { + "$ref": "#/definitions/GoldenActivable" + }, + { + "$ref": "#/definitions/GoldenTimestampable" + } + ] + } + }, + "type": "object" + }, + "GoldenTimestampable": { + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenIntersection", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_item.json b/tests/Functional/JsonSchema/golden/output_item.json new file mode 100644 index 00000000000..ee3554c92a1 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_item.json @@ -0,0 +1,37 @@ +{ + "definitions": { + "GoldenBook-golden.read": { + "deprecated": true, + "description": "Retrieves a golden book.", + "externalDocs": { + "url": "https://schema.org/Book" + }, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "isbn": { + "deprecated": true, + "description": "International standard book number.", + "example": "9780000000000", + "type": [ + "string", + "null" + ] + }, + "title": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenBook-golden.read", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_nested_attributes.json b/tests/Functional/JsonSchema/golden/output_nested_attributes.json new file mode 100644 index 00000000000..778b46f0326 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_nested_attributes.json @@ -0,0 +1,36 @@ +{ + "definitions": { + "GoldenAuthor-name": { + "properties": { + "name": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenNested-name_author.name": { + "properties": { + "author": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenAuthor-name" + }, + { + "type": "null" + } + ] + }, + "name": { + "default": "", + "type": "string" + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenNested-name_author.name", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Functional/JsonSchema/golden/output_relation_iri_vs_embed.json b/tests/Functional/JsonSchema/golden/output_relation_iri_vs_embed.json new file mode 100644 index 00000000000..2fca9b3fc47 --- /dev/null +++ b/tests/Functional/JsonSchema/golden/output_relation_iri_vs_embed.json @@ -0,0 +1,52 @@ +{ + "definitions": { + "GoldenRelated-golden.read_golden.embed": { + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "label": { + "default": "", + "type": "string" + } + }, + "type": "object" + }, + "GoldenRelation-golden.read_golden.embed": { + "properties": { + "embedded": { + "anyOf": [ + { + "$ref": "#/definitions/GoldenRelated-golden.read_golden.embed" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": [ + "integer", + "null" + ] + }, + "linked": { + "example": "https://example.com/", + "format": "iri-reference", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + } + }, + "schema": { + "$ref": "#/definitions/GoldenRelation-golden.read_golden.embed", + "$schema": "http://json-schema.org/draft-07/schema#" + } +} diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 0d47ca31b71..5b19d7540a8 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -99,6 +99,9 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'json' => ['mime_types' => ['application/problem+json', 'application/json']], ], 'jsonschema_formats' => [], + 'json_schema' => [ + 'generator' => false, + ], 'exception_to_status' => [ ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,