Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions features/hydra/docs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ Feature: Documentation support
And the JSON node "hydra:description" should contain "Made with love"
And the JSON node "hydra:entrypoint" should be equal to "/"
# Supported classes
And the Hydra class "The API entrypoint" exists
And the Hydra class "A constraint violation" exists
And the Hydra class "A constraint violation list" exists
And the Hydra class "Entrypoint" exists
And the Hydra class "ConstraintViolation" exists
And the Hydra class "ConstraintViolationList" exists
And the Hydra class "CircularReference" exists
And the Hydra class "CustomIdentifierDummy" exists
And the Hydra class "CustomNormalizedDummy" exists
Expand All @@ -49,7 +49,6 @@ Feature: Documentation support
# Doc
And the value of the node "@id" of the Hydra class "Dummy" is "#Dummy"
And the value of the node "@type" of the Hydra class "Dummy" is "hydra:Class"
And the value of the node "rdfs:label" of the Hydra class "Dummy" is "Dummy"
And the value of the node "hydra:title" of the Hydra class "Dummy" is "Dummy"
And the value of the node "hydra:description" of the Hydra class "Dummy" is "Dummy."
# Properties
Expand All @@ -62,7 +61,7 @@ Feature: Documentation support
And the value of the node "@type" of the property "name" of the Hydra class "Dummy" is "hydra:SupportedProperty"
And the value of the node "hydra:property.@id" of the property "name" of the Hydra class "Dummy" is "https://schema.org/name"
And the value of the node "hydra:property.@type" of the property "name" of the Hydra class "Dummy" is "rdf:Property"
And the value of the node "hydra:property.rdfs:label" of the property "name" of the Hydra class "Dummy" is "name"
And the value of the node "hydra:property.label" of the property "name" of the Hydra class "Dummy" is "name"
And the value of the node "hydra:property.domain" of the property "name" of the Hydra class "Dummy" is "#Dummy"
And the value of the node "hydra:property.range" of the property "name" of the Hydra class "Dummy" is "xmls:string"
And the value of the node "hydra:property.range" of the property "relatedDummy" of the Hydra class "Dummy" is "https://schema.org/Product"
Expand All @@ -74,14 +73,16 @@ Feature: Documentation support
And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "hydra:Operation"
And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "schema:FindAction"
And the value of the node "hydra:method" of the operation "GET" of the Hydra class "Dummy" is "GET"
And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource."
And the value of the node "rdfs:label" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource."
And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "getDummy"
And the value of the node "hydra:description" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource."
And the value of the node "returns" of the operation "GET" of the Hydra class "Dummy" is "Dummy"
And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource."
And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource."
And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "putDummy"
And the value of the node "hydra:description" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource."
And the value of the node "hydra:description" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource."
And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "deleteDummy"
And the value of the node "returns" of the operation "DELETE" of the Hydra class "Dummy" is "owl:Nothing"
# Deprecations
And the boolean value of the node "owl:deprecated" of the Hydra class "DeprecatedResource" is true
And the boolean value of the node "hydra:property.owl:deprecated" of the property "deprecatedField" of the Hydra class "DeprecatedResource" is true
And the boolean value of the node "owl:deprecated" of the property "The collection of DeprecatedResource resources" of the Hydra class "The API entrypoint" is true
And the boolean value of the node "owl:deprecated" of the property "getDeprecatedResourceCollection" of the Hydra class "Entrypoint" is true
And the boolean value of the node "owl:deprecated" of the operation "GET" of the Hydra class "DeprecatedResource" is true
4 changes: 2 additions & 2 deletions features/hydra/error.feature
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ Feature: Error handling
And the JSON should be equal to:
"""
{
"@context": "/contexts/ConstraintViolationList",
"@context": "/contexts/ConstraintViolation",
"@id": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3",
"@type": "ConstraintViolationList",
"@type": "ConstraintViolation",
"status": 422,
"violations": [
{
Expand Down
12 changes: 6 additions & 6 deletions features/main/validation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ Feature: Using validations groups
And the JSON should be a superset of:
"""
{
"@context": "/contexts/ConstraintViolationList",
"@type": "ConstraintViolationList",
"@context": "/contexts/ConstraintViolation",
"@type": "ConstraintViolation",
"title": "An error occurred",
"description": "name: This value should not be null.",
"violations": [
Expand Down Expand Up @@ -58,8 +58,8 @@ Feature: Using validations groups
And the JSON should be a superset of:
"""
{
"@context": "/contexts/ConstraintViolationList",
"@type": "ConstraintViolationList",
"@context": "/contexts/ConstraintViolation",
"@type": "ConstraintViolation",
"title": "An error occurred",
"description": "title: This value should not be null.",
"violations": [
Expand Down Expand Up @@ -111,8 +111,8 @@ Feature: Using validations groups
And the JSON should be a superset of:
"""
{
"@context": "/contexts/ConstraintViolationList",
"@type": "ConstraintViolationList",
"@context": "/contexts/ConstraintViolation",
"@type": "ConstraintViolation",
"hydra:title": "An error occurred",
"hydra:description": "baz: This value should be of type string.\nqux: This value should be of type string.\nfoo: This value should be of type bool.\nbar: This value should be of type int.\nuuid: This value should be of type uuid.\nrelatedDummy: This value should be of type array|string.\nrelatedDummies: This value should be of type array.",
"violations": [
Expand Down
103 changes: 46 additions & 57 deletions src/Hydra/Serializer/DocumentationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Validator\Exception\ValidationException;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
Expand Down Expand Up @@ -71,17 +70,13 @@ public function normalize(mixed $object, ?string $format = null, array $context
$resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);

$resourceMetadata = $resourceMetadataCollection[0];
if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) {
continue;
}

if (true === $resourceMetadata->getHideHydraOperation()) {
continue;
}

$shortName = $resourceMetadata->getShortName();

$prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";

$this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection);
$classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection);
}
Expand All @@ -105,8 +100,8 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str
'@id' => \sprintf('#Entrypoint/%s', lcfirst($shortName)),
'@type' => $hydraPrefix.'Link',
'domain' => '#Entrypoint',
'rdfs:label' => "The collection of $shortName resources",
'rdfs:range' => [
'owl:maxCardinality' => 1,
'range' => [
['@id' => $hydraPrefix.'Collection'],
[
'owl:equivalentClass' => [
Expand All @@ -117,7 +112,8 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str
],
$hydraPrefix.'supportedOperation' => $hydraCollectionOperations,
],
$hydraPrefix.'title' => "The collection of $shortName resources",
$hydraPrefix.'title' => "get{$shortName}Collection",
$hydraPrefix.'description' => "The collection of $shortName resources",
$hydraPrefix.'readable' => true,
$hydraPrefix.'writeable' => false,
];
Expand All @@ -140,7 +136,6 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata,
$class = [
'@id' => $prefixedShortName,
'@type' => $hydraPrefix.'Class',
'rdfs:label' => $shortName,
$hydraPrefix.'title' => $shortName,
$hydraPrefix.'supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix),
$hydraPrefix.'supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection, $hydraPrefix),
Expand All @@ -150,6 +145,10 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata,
$class[$hydraPrefix.'description'] = $description;
}

if ($resourceMetadata instanceof ErrorResource) {
$class['subClassOf'] = 'Error';
}

if ($isDeprecated) {
$class['owl:deprecated'] = true;
}
Expand Down Expand Up @@ -232,6 +231,10 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource
$propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);
}

if (false === $propertyMetadata->getHydra()) {
continue;
}

$properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix);
}
}
Expand All @@ -254,6 +257,7 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio
if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
continue;
}

$hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
}
}
Expand Down Expand Up @@ -283,49 +287,57 @@ private function getHydraOperation(HttpOperation $operation, string $prefixedSho
if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
$hydraOperation += [
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
$hydraPrefix.'title' => "Retrieves the collection of $shortName resources.",
$hydraPrefix.'description' => "Retrieves the collection of $shortName resources.",
'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection',
];
} elseif ('GET' === $method) {
$hydraOperation += [
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
$hydraPrefix.'title' => "Retrieves a $shortName resource.",
$hydraPrefix.'description' => "Retrieves a $shortName resource.",
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
];
} elseif ('PATCH' === $method) {
$hydraOperation += [
'@type' => $hydraPrefix.'Operation',
$hydraPrefix.'title' => "Updates the $shortName resource.",
$hydraPrefix.'description' => "Updates the $shortName resource.",
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
];

if (null !== $inputClass) {
$possibleValue = [];
foreach ($operation->getInputFormats() as $mimeTypes) {
foreach ($mimeTypes as $mimeType) {
$possibleValue[] = $mimeType;
}
}

$hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]];
}
} elseif ('POST' === $method) {
$hydraOperation += [
'@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'],
$hydraPrefix.'title' => "Creates a $shortName resource.",
$hydraPrefix.'description' => "Creates a $shortName resource.",
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
];
} elseif ('PUT' === $method) {
$hydraOperation += [
'@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'],
$hydraPrefix.'title' => "Replaces the $shortName resource.",
$hydraPrefix.'description' => "Replaces the $shortName resource.",
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
];
} elseif ('DELETE' === $method) {
$hydraOperation += [
'@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'],
$hydraPrefix.'title' => "Deletes the $shortName resource.",
$hydraPrefix.'description' => "Deletes the $shortName resource.",
'returns' => 'owl:Nothing',
];
}

$hydraOperation[$hydraPrefix.'method'] ?? $hydraOperation[$hydraPrefix.'method'] = $method;

if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation[$hydraPrefix.'title'])) {
$hydraOperation['rdfs:label'] = $hydraOperation[$hydraPrefix.'title'];
}
$hydraOperation[$hydraPrefix.'method'] ??= $method;
$hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : '');

ksort($hydraOperation);

Expand Down Expand Up @@ -434,29 +446,30 @@ private function getClasses(array $entrypointProperties, array $classes, string
$classes[] = [
'@id' => '#Entrypoint',
'@type' => $hydraPrefix.'Class',
$hydraPrefix.'title' => 'The API entrypoint',
$hydraPrefix.'title' => 'Entrypoint',
$hydraPrefix.'supportedProperty' => $entrypointProperties,
$hydraPrefix.'supportedOperation' => [
'@type' => $hydraPrefix.'Operation',
$hydraPrefix.'method' => 'GET',
'rdfs:label' => 'The API entrypoint.',
'returns' => 'EntryPoint',
$hydraPrefix.'title' => 'index',
$hydraPrefix.'description' => 'The API Entrypoint.',
$hydraPrefix.'returns' => 'Entrypoint',
],
];

// Constraint violation
$classes[] = [
'@id' => '#ConstraintViolation',
'@id' => '#ConstraintViolationList',
'@type' => $hydraPrefix.'Class',
$hydraPrefix.'title' => 'A constraint violation',
$hydraPrefix.'title' => 'ConstraintViolationList',
$hydraPrefix.'description' => 'A constraint violation List.',
$hydraPrefix.'supportedProperty' => [
[
'@type' => $hydraPrefix.'SupportedProperty',
$hydraPrefix.'property' => [
'@id' => '#ConstraintViolation/propertyPath',
'@id' => '#ConstraintViolationList/propertyPath',
'@type' => 'rdf:Property',
'rdfs:label' => 'propertyPath',
'domain' => '#ConstraintViolation',
'domain' => '#ConstraintViolationList',
'range' => 'xmls:string',
],
$hydraPrefix.'title' => 'propertyPath',
Expand All @@ -467,10 +480,10 @@ private function getClasses(array $entrypointProperties, array $classes, string
[
'@type' => $hydraPrefix.'SupportedProperty',
$hydraPrefix.'property' => [
'@id' => '#ConstraintViolation/message',
'@id' => '#ConstraintViolationList/message',
'@type' => 'rdf:Property',
'rdfs:label' => 'message',
'domain' => '#ConstraintViolation',
'domain' => '#ConstraintViolationList',
'range' => 'xmls:string',
],
$hydraPrefix.'title' => 'message',
Expand All @@ -481,30 +494,6 @@ private function getClasses(array $entrypointProperties, array $classes, string
],
];

// Constraint violation list
$classes[] = [
'@id' => '#ConstraintViolationList',
'@type' => $hydraPrefix.'Class',
'subClassOf' => $hydraPrefix.'Error',
$hydraPrefix.'title' => 'A constraint violation list',
$hydraPrefix.'supportedProperty' => [
[
'@type' => $hydraPrefix.'SupportedProperty',
$hydraPrefix.'property' => [
'@id' => '#ConstraintViolationList/violations',
'@type' => 'rdf:Property',
'rdfs:label' => 'violations',
'domain' => '#ConstraintViolationList',
'range' => '#ConstraintViolation',
],
$hydraPrefix.'title' => 'violations',
$hydraPrefix.'description' => 'The violations',
$hydraPrefix.'readable' => true,
$hydraPrefix.'writeable' => false,
],
],
];

return $classes;
}

Expand All @@ -524,8 +513,8 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName
$propertyData = ($propertyMetadata->getJsonldContext()[$hydraPrefix.'property'] ?? []) + [
'@id' => $iri,
'@type' => false === $propertyMetadata->isReadableLink() ? $hydraPrefix.'Link' : 'rdf:Property',
'rdfs:label' => $propertyName,
'domain' => $prefixedShortName,
'label' => $propertyName,
];

if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) {
Expand All @@ -544,7 +533,7 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName
'@type' => $hydraPrefix.'SupportedProperty',
$hydraPrefix.'property' => $propertyData,
$hydraPrefix.'title' => $propertyName,
$hydraPrefix.'required' => $propertyMetadata->isRequired(),
$hydraPrefix.'required' => $propertyMetadata->isRequired() ?? false,
$hydraPrefix.'readable' => $propertyMetadata->isReadable(),
$hydraPrefix.'writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
];
Expand Down
Loading
Loading