Skip to content

Commit

Permalink
fix(hydra): rdfs:label should not duplicate title
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Jan 13, 2025
1 parent 01fd742 commit d683f79
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 74 deletions.
20 changes: 10 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,6 @@ 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.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 +72,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
102 changes: 46 additions & 56 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,9 @@ 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' => [
'title' => "{$shortName}CollectionEntrypoint",
'owl:maxCardinality' => 1,
'range' => [
['@id' => $hydraPrefix.'Collection'],
[
'owl:equivalentClass' => [
Expand All @@ -117,7 +113,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 +137,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 +146,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 +232,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 +258,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 +288,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 +447,30 @@ private function getClasses(array $entrypointProperties, array $classes, string
$classes[] = [
'@id' => '#Entrypoint',
'@type' => $hydraPrefix.'Class',
$hydraPrefix.'title' => 'The API entrypoint',
$hydraPrefix.'title' => 'Entrypoint',
$hydraPrefix.'description' => 'API Entrypoint',
$hydraPrefix.'supportedProperty' => $entrypointProperties,
$hydraPrefix.'supportedOperation' => [
'@type' => $hydraPrefix.'Operation',
$hydraPrefix.'method' => 'GET',
'rdfs:label' => 'The API entrypoint.',
'returns' => 'EntryPoint',
$hydraPrefix.'title' => 'Entrypoint',
'rdfs:label' => 'index',
$hydraPrefix.'returns' => 'Entrypoint',
],
];

// Constraint violation
$classes[] = [
'@id' => '#ConstraintViolation',
'@id' => '#ConstraintViolationList',
'@type' => $hydraPrefix.'Class',
$hydraPrefix.'title' => 'A constraint violation',
$hydraPrefix.'title' => 'ConstraintViolationList',
$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 +481,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 +495,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,7 +514,7 @@ 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,
'title' => $propertyName,
'domain' => $prefixedShortName,
];

Expand Down
21 changes: 19 additions & 2 deletions src/Metadata/ApiProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*
* @author Kévin Dunglas <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS)]
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
final class ApiProperty
{
private ?array $types;
Expand Down Expand Up @@ -216,6 +216,10 @@ public function __construct(
private ?string $property = null,
private ?string $policy = null,
array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|null $serialize = null,
/**
* Whether to document this property as a hydra:supportedProperty.
*/
private ?bool $hydra = null,
private array $extraProperties = [],
) {
$this->types = \is_string($types) ? (array) $types : $types;
Expand Down Expand Up @@ -517,7 +521,7 @@ public function withSchema(array $schema = []): self
return $self;
}

public function withInitializable(?bool $initializable): self
public function withInitializable(bool $initializable): self
{
$self = clone $this;
$self->initializable = $initializable;
Expand Down Expand Up @@ -626,4 +630,17 @@ public function withSerialize(array|Context|Groups|Ignore|SerializedName|Seriali

return $self;
}

public function getHydra(): ?bool
{
return $this->hydra;
}

public function withHydra(bool $hydra): static
{
$self = clone $this;
$self->hydra = $hydra;

return $self;
}
}
2 changes: 2 additions & 0 deletions src/Metadata/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public function __construct(
$provider = null,
$processor = null,
?OptionsInterface $stateOptions = null,
?bool $hideHydraOperation = null,
array $extraProperties = [],
) {
parent::__construct(
Expand Down Expand Up @@ -169,6 +170,7 @@ class: $class,
provider: $provider,
processor: $processor,
stateOptions: $stateOptions,
hideHydraOperation: $hideHydraOperation,
extraProperties: $extraProperties,
);
}
Expand Down
Loading

0 comments on commit d683f79

Please sign in to comment.