Skip to content

Commit db40a63

Browse files
authored
fix(hydra): rdfs:label should not duplicate title (#6748)
1 parent 01fd742 commit db40a63

File tree

14 files changed

+252
-411
lines changed

14 files changed

+252
-411
lines changed

features/hydra/docs.feature

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ Feature: Documentation support
3333
And the JSON node "hydra:description" should contain "Made with love"
3434
And the JSON node "hydra:entrypoint" should be equal to "/"
3535
# Supported classes
36-
And the Hydra class "The API entrypoint" exists
37-
And the Hydra class "A constraint violation" exists
38-
And the Hydra class "A constraint violation list" exists
36+
And the Hydra class "Entrypoint" exists
37+
And the Hydra class "ConstraintViolation" exists
38+
And the Hydra class "ConstraintViolationList" exists
3939
And the Hydra class "CircularReference" exists
4040
And the Hydra class "CustomIdentifierDummy" exists
4141
And the Hydra class "CustomNormalizedDummy" exists
@@ -49,7 +49,6 @@ Feature: Documentation support
4949
# Doc
5050
And the value of the node "@id" of the Hydra class "Dummy" is "#Dummy"
5151
And the value of the node "@type" of the Hydra class "Dummy" is "hydra:Class"
52-
And the value of the node "rdfs:label" of the Hydra class "Dummy" is "Dummy"
5352
And the value of the node "hydra:title" of the Hydra class "Dummy" is "Dummy"
5453
And the value of the node "hydra:description" of the Hydra class "Dummy" is "Dummy."
5554
# Properties
@@ -62,7 +61,7 @@ Feature: Documentation support
6261
And the value of the node "@type" of the property "name" of the Hydra class "Dummy" is "hydra:SupportedProperty"
6362
And the value of the node "hydra:property.@id" of the property "name" of the Hydra class "Dummy" is "https://schema.org/name"
6463
And the value of the node "hydra:property.@type" of the property "name" of the Hydra class "Dummy" is "rdf:Property"
65-
And the value of the node "hydra:property.rdfs:label" of the property "name" of the Hydra class "Dummy" is "name"
64+
And the value of the node "hydra:property.label" of the property "name" of the Hydra class "Dummy" is "name"
6665
And the value of the node "hydra:property.domain" of the property "name" of the Hydra class "Dummy" is "#Dummy"
6766
And the value of the node "hydra:property.range" of the property "name" of the Hydra class "Dummy" is "xmls:string"
6867
And the value of the node "hydra:property.range" of the property "relatedDummy" of the Hydra class "Dummy" is "https://schema.org/Product"
@@ -74,14 +73,16 @@ Feature: Documentation support
7473
And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "hydra:Operation"
7574
And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "schema:FindAction"
7675
And the value of the node "hydra:method" of the operation "GET" of the Hydra class "Dummy" is "GET"
77-
And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource."
78-
And the value of the node "rdfs:label" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource."
76+
And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "getDummy"
77+
And the value of the node "hydra:description" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource."
7978
And the value of the node "returns" of the operation "GET" of the Hydra class "Dummy" is "Dummy"
80-
And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource."
81-
And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource."
79+
And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "putDummy"
80+
And the value of the node "hydra:description" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource."
81+
And the value of the node "hydra:description" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource."
82+
And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "deleteDummy"
8283
And the value of the node "returns" of the operation "DELETE" of the Hydra class "Dummy" is "owl:Nothing"
8384
# Deprecations
8485
And the boolean value of the node "owl:deprecated" of the Hydra class "DeprecatedResource" is true
8586
And the boolean value of the node "hydra:property.owl:deprecated" of the property "deprecatedField" of the Hydra class "DeprecatedResource" is true
86-
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
87+
And the boolean value of the node "owl:deprecated" of the property "getDeprecatedResourceCollection" of the Hydra class "Entrypoint" is true
8788
And the boolean value of the node "owl:deprecated" of the operation "GET" of the Hydra class "DeprecatedResource" is true

features/hydra/error.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ Feature: Error handling
3737
And the JSON should be equal to:
3838
"""
3939
{
40-
"@context": "/contexts/ConstraintViolationList",
40+
"@context": "/contexts/ConstraintViolation",
4141
"@id": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3",
42-
"@type": "ConstraintViolationList",
42+
"@type": "ConstraintViolation",
4343
"status": 422,
4444
"violations": [
4545
{

features/main/validation.feature

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ Feature: Using validations groups
2929
And the JSON should be a superset of:
3030
"""
3131
{
32-
"@context": "/contexts/ConstraintViolationList",
33-
"@type": "ConstraintViolationList",
32+
"@context": "/contexts/ConstraintViolation",
33+
"@type": "ConstraintViolation",
3434
"title": "An error occurred",
3535
"description": "name: This value should not be null.",
3636
"violations": [
@@ -58,8 +58,8 @@ Feature: Using validations groups
5858
And the JSON should be a superset of:
5959
"""
6060
{
61-
"@context": "/contexts/ConstraintViolationList",
62-
"@type": "ConstraintViolationList",
61+
"@context": "/contexts/ConstraintViolation",
62+
"@type": "ConstraintViolation",
6363
"title": "An error occurred",
6464
"description": "title: This value should not be null.",
6565
"violations": [
@@ -111,8 +111,8 @@ Feature: Using validations groups
111111
And the JSON should be a superset of:
112112
"""
113113
{
114-
"@context": "/contexts/ConstraintViolationList",
115-
"@type": "ConstraintViolationList",
114+
"@context": "/contexts/ConstraintViolation",
115+
"@type": "ConstraintViolation",
116116
"hydra:title": "An error occurred",
117117
"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.",
118118
"violations": [

src/Hydra/Serializer/DocumentationNormalizer.php

Lines changed: 46 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
3030
use ApiPlatform\Metadata\ResourceClassResolverInterface;
3131
use ApiPlatform\Metadata\UrlGeneratorInterface;
32-
use ApiPlatform\Validator\Exception\ValidationException;
3332
use Symfony\Component\PropertyInfo\Type;
3433
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3534
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@@ -71,17 +70,13 @@ public function normalize(mixed $object, ?string $format = null, array $context
7170
$resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
7271

7372
$resourceMetadata = $resourceMetadataCollection[0];
74-
if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) {
75-
continue;
76-
}
77-
7873
if (true === $resourceMetadata->getHideHydraOperation()) {
7974
continue;
8075
}
8176

8277
$shortName = $resourceMetadata->getShortName();
83-
8478
$prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";
79+
8580
$this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection);
8681
$classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection);
8782
}
@@ -105,8 +100,8 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str
105100
'@id' => \sprintf('#Entrypoint/%s', lcfirst($shortName)),
106101
'@type' => $hydraPrefix.'Link',
107102
'domain' => '#Entrypoint',
108-
'rdfs:label' => "The collection of $shortName resources",
109-
'rdfs:range' => [
103+
'owl:maxCardinality' => 1,
104+
'range' => [
110105
['@id' => $hydraPrefix.'Collection'],
111106
[
112107
'owl:equivalentClass' => [
@@ -117,7 +112,8 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str
117112
],
118113
$hydraPrefix.'supportedOperation' => $hydraCollectionOperations,
119114
],
120-
$hydraPrefix.'title' => "The collection of $shortName resources",
115+
$hydraPrefix.'title' => "get{$shortName}Collection",
116+
$hydraPrefix.'description' => "The collection of $shortName resources",
121117
$hydraPrefix.'readable' => true,
122118
$hydraPrefix.'writeable' => false,
123119
];
@@ -140,7 +136,6 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata,
140136
$class = [
141137
'@id' => $prefixedShortName,
142138
'@type' => $hydraPrefix.'Class',
143-
'rdfs:label' => $shortName,
144139
$hydraPrefix.'title' => $shortName,
145140
$hydraPrefix.'supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix),
146141
$hydraPrefix.'supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection, $hydraPrefix),
@@ -150,6 +145,10 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata,
150145
$class[$hydraPrefix.'description'] = $description;
151146
}
152147

148+
if ($resourceMetadata instanceof ErrorResource) {
149+
$class['subClassOf'] = 'Error';
150+
}
151+
153152
if ($isDeprecated) {
154153
$class['owl:deprecated'] = true;
155154
}
@@ -232,6 +231,10 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource
232231
$propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);
233232
}
234233

234+
if (false === $propertyMetadata->getHydra()) {
235+
continue;
236+
}
237+
235238
$properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix);
236239
}
237240
}
@@ -254,6 +257,7 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio
254257
if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
255258
continue;
256259
}
260+
257261
$hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
258262
}
259263
}
@@ -283,49 +287,57 @@ private function getHydraOperation(HttpOperation $operation, string $prefixedSho
283287
if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
284288
$hydraOperation += [
285289
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
286-
$hydraPrefix.'title' => "Retrieves the collection of $shortName resources.",
290+
$hydraPrefix.'description' => "Retrieves the collection of $shortName resources.",
287291
'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection',
288292
];
289293
} elseif ('GET' === $method) {
290294
$hydraOperation += [
291295
'@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
292-
$hydraPrefix.'title' => "Retrieves a $shortName resource.",
296+
$hydraPrefix.'description' => "Retrieves a $shortName resource.",
293297
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
294298
];
295299
} elseif ('PATCH' === $method) {
296300
$hydraOperation += [
297301
'@type' => $hydraPrefix.'Operation',
298-
$hydraPrefix.'title' => "Updates the $shortName resource.",
302+
$hydraPrefix.'description' => "Updates the $shortName resource.",
299303
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
300304
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
301305
];
306+
307+
if (null !== $inputClass) {
308+
$possibleValue = [];
309+
foreach ($operation->getInputFormats() as $mimeTypes) {
310+
foreach ($mimeTypes as $mimeType) {
311+
$possibleValue[] = $mimeType;
312+
}
313+
}
314+
315+
$hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]];
316+
}
302317
} elseif ('POST' === $method) {
303318
$hydraOperation += [
304319
'@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'],
305-
$hydraPrefix.'title' => "Creates a $shortName resource.",
320+
$hydraPrefix.'description' => "Creates a $shortName resource.",
306321
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
307322
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
308323
];
309324
} elseif ('PUT' === $method) {
310325
$hydraOperation += [
311326
'@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'],
312-
$hydraPrefix.'title' => "Replaces the $shortName resource.",
327+
$hydraPrefix.'description' => "Replaces the $shortName resource.",
313328
'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
314329
'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
315330
];
316331
} elseif ('DELETE' === $method) {
317332
$hydraOperation += [
318333
'@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'],
319-
$hydraPrefix.'title' => "Deletes the $shortName resource.",
334+
$hydraPrefix.'description' => "Deletes the $shortName resource.",
320335
'returns' => 'owl:Nothing',
321336
];
322337
}
323338

324-
$hydraOperation[$hydraPrefix.'method'] ?? $hydraOperation[$hydraPrefix.'method'] = $method;
325-
326-
if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation[$hydraPrefix.'title'])) {
327-
$hydraOperation['rdfs:label'] = $hydraOperation[$hydraPrefix.'title'];
328-
}
339+
$hydraOperation[$hydraPrefix.'method'] ??= $method;
340+
$hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : '');
329341

330342
ksort($hydraOperation);
331343

@@ -434,29 +446,30 @@ private function getClasses(array $entrypointProperties, array $classes, string
434446
$classes[] = [
435447
'@id' => '#Entrypoint',
436448
'@type' => $hydraPrefix.'Class',
437-
$hydraPrefix.'title' => 'The API entrypoint',
449+
$hydraPrefix.'title' => 'Entrypoint',
438450
$hydraPrefix.'supportedProperty' => $entrypointProperties,
439451
$hydraPrefix.'supportedOperation' => [
440452
'@type' => $hydraPrefix.'Operation',
441453
$hydraPrefix.'method' => 'GET',
442-
'rdfs:label' => 'The API entrypoint.',
443-
'returns' => 'EntryPoint',
454+
$hydraPrefix.'title' => 'index',
455+
$hydraPrefix.'description' => 'The API Entrypoint.',
456+
$hydraPrefix.'returns' => 'Entrypoint',
444457
],
445458
];
446459

447-
// Constraint violation
448460
$classes[] = [
449-
'@id' => '#ConstraintViolation',
461+
'@id' => '#ConstraintViolationList',
450462
'@type' => $hydraPrefix.'Class',
451-
$hydraPrefix.'title' => 'A constraint violation',
463+
$hydraPrefix.'title' => 'ConstraintViolationList',
464+
$hydraPrefix.'description' => 'A constraint violation List.',
452465
$hydraPrefix.'supportedProperty' => [
453466
[
454467
'@type' => $hydraPrefix.'SupportedProperty',
455468
$hydraPrefix.'property' => [
456-
'@id' => '#ConstraintViolation/propertyPath',
469+
'@id' => '#ConstraintViolationList/propertyPath',
457470
'@type' => 'rdf:Property',
458471
'rdfs:label' => 'propertyPath',
459-
'domain' => '#ConstraintViolation',
472+
'domain' => '#ConstraintViolationList',
460473
'range' => 'xmls:string',
461474
],
462475
$hydraPrefix.'title' => 'propertyPath',
@@ -467,10 +480,10 @@ private function getClasses(array $entrypointProperties, array $classes, string
467480
[
468481
'@type' => $hydraPrefix.'SupportedProperty',
469482
$hydraPrefix.'property' => [
470-
'@id' => '#ConstraintViolation/message',
483+
'@id' => '#ConstraintViolationList/message',
471484
'@type' => 'rdf:Property',
472485
'rdfs:label' => 'message',
473-
'domain' => '#ConstraintViolation',
486+
'domain' => '#ConstraintViolationList',
474487
'range' => 'xmls:string',
475488
],
476489
$hydraPrefix.'title' => 'message',
@@ -481,30 +494,6 @@ private function getClasses(array $entrypointProperties, array $classes, string
481494
],
482495
];
483496

484-
// Constraint violation list
485-
$classes[] = [
486-
'@id' => '#ConstraintViolationList',
487-
'@type' => $hydraPrefix.'Class',
488-
'subClassOf' => $hydraPrefix.'Error',
489-
$hydraPrefix.'title' => 'A constraint violation list',
490-
$hydraPrefix.'supportedProperty' => [
491-
[
492-
'@type' => $hydraPrefix.'SupportedProperty',
493-
$hydraPrefix.'property' => [
494-
'@id' => '#ConstraintViolationList/violations',
495-
'@type' => 'rdf:Property',
496-
'rdfs:label' => 'violations',
497-
'domain' => '#ConstraintViolationList',
498-
'range' => '#ConstraintViolation',
499-
],
500-
$hydraPrefix.'title' => 'violations',
501-
$hydraPrefix.'description' => 'The violations',
502-
$hydraPrefix.'readable' => true,
503-
$hydraPrefix.'writeable' => false,
504-
],
505-
],
506-
];
507-
508497
return $classes;
509498
}
510499

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

531520
if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) {
@@ -544,7 +533,7 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName
544533
'@type' => $hydraPrefix.'SupportedProperty',
545534
$hydraPrefix.'property' => $propertyData,
546535
$hydraPrefix.'title' => $propertyName,
547-
$hydraPrefix.'required' => $propertyMetadata->isRequired(),
536+
$hydraPrefix.'required' => $propertyMetadata->isRequired() ?? false,
548537
$hydraPrefix.'readable' => $propertyMetadata->isReadable(),
549538
$hydraPrefix.'writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
550539
];

0 commit comments

Comments
 (0)