Skip to content

Commit 21639b9

Browse files
committed
fix(hydra): resolve related class for enum and read-only resources
Adds a fallback strategy to findRelatedClass() that checks rdfs:range for direct @id class references when owl:equivalentClass and supportedOperation lookups both fail. Fixes api-platform/core#7222
1 parent af8b430 commit 21639b9

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

src/hydra/parseHydraDocumentation.test.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,3 +1599,234 @@ test("Resource parameters can be retrieved", async () => {
15991599
},
16001600
]);
16011601
});
1602+
1603+
test("parse a Hydra documentation with enum/read-only resources (rdfs:range direct @id)", async () => {
1604+
const enumEntrypoint = {
1605+
"@context": {
1606+
"@vocab": "http://localhost/docs.jsonld#",
1607+
hydra: "http://www.w3.org/ns/hydra/core#",
1608+
book: {
1609+
"@id": "Entrypoint/book",
1610+
"@type": "@id",
1611+
},
1612+
bookCondition: {
1613+
"@id": "Entrypoint/bookCondition",
1614+
"@type": "@id",
1615+
},
1616+
},
1617+
"@id": "/",
1618+
"@type": "Entrypoint",
1619+
book: "/books",
1620+
bookCondition: "/book_conditions",
1621+
};
1622+
1623+
const enumDocs = {
1624+
"@context": {
1625+
"@vocab": "http://localhost/docs.jsonld#",
1626+
hydra: "http://www.w3.org/ns/hydra/core#",
1627+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
1628+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
1629+
xmls: "http://www.w3.org/2001/XMLSchema#",
1630+
owl: "http://www.w3.org/2002/07/owl#",
1631+
domain: {
1632+
"@id": "rdfs:domain",
1633+
"@type": "@id",
1634+
},
1635+
range: {
1636+
"@id": "rdfs:range",
1637+
"@type": "@id",
1638+
},
1639+
subClassOf: {
1640+
"@id": "rdfs:subClassOf",
1641+
"@type": "@id",
1642+
},
1643+
expects: {
1644+
"@id": "hydra:expects",
1645+
"@type": "@id",
1646+
},
1647+
returns: {
1648+
"@id": "hydra:returns",
1649+
"@type": "@id",
1650+
},
1651+
},
1652+
"@id": "/docs.jsonld",
1653+
"hydra:title": "API with enums",
1654+
"hydra:description": "An API that exposes enum resources",
1655+
"hydra:entrypoint": "/",
1656+
"hydra:supportedClass": [
1657+
{
1658+
"@id": "http://schema.org/Book",
1659+
"@type": "hydra:Class",
1660+
"rdfs:label": "Book",
1661+
"hydra:title": "Book",
1662+
"hydra:supportedProperty": [
1663+
{
1664+
"@type": "hydra:SupportedProperty",
1665+
"hydra:property": {
1666+
"@id": "http://schema.org/name",
1667+
"@type": "rdf:Property",
1668+
"rdfs:label": "name",
1669+
domain: "http://schema.org/Book",
1670+
range: "xmls:string",
1671+
},
1672+
"hydra:title": "name",
1673+
"hydra:required": true,
1674+
"hydra:readable": true,
1675+
"hydra:writeable": true,
1676+
},
1677+
],
1678+
"hydra:supportedOperation": [
1679+
{
1680+
"@type": "hydra:Operation",
1681+
"hydra:method": "GET",
1682+
"hydra:title": "Retrieves Book resource.",
1683+
"rdfs:label": "Retrieves Book resource.",
1684+
returns: "http://schema.org/Book",
1685+
},
1686+
],
1687+
},
1688+
{
1689+
"@id": "#BookCondition",
1690+
"@type": "hydra:Class",
1691+
"rdfs:label": "BookCondition",
1692+
"hydra:title": "BookCondition",
1693+
"hydra:description": "The condition of a book (new, used, damaged).",
1694+
"hydra:supportedProperty": [
1695+
{
1696+
"@type": "hydra:SupportedProperty",
1697+
"hydra:property": {
1698+
"@id": "#BookCondition/value",
1699+
"@type": "rdf:Property",
1700+
"rdfs:label": "value",
1701+
domain: "#BookCondition",
1702+
range: "xmls:string",
1703+
},
1704+
"hydra:title": "value",
1705+
"hydra:required": true,
1706+
"hydra:readable": true,
1707+
"hydra:writeable": false,
1708+
},
1709+
],
1710+
"hydra:supportedOperation": [
1711+
{
1712+
"@type": "hydra:Operation",
1713+
"hydra:method": "GET",
1714+
"hydra:title": "Retrieves BookCondition resource.",
1715+
"rdfs:label": "Retrieves BookCondition resource.",
1716+
returns: "#BookCondition",
1717+
},
1718+
],
1719+
},
1720+
{
1721+
"@id": "#Entrypoint",
1722+
"@type": "hydra:Class",
1723+
"hydra:title": "The API entrypoint",
1724+
"hydra:supportedProperty": [
1725+
{
1726+
"@type": "hydra:SupportedProperty",
1727+
"hydra:property": {
1728+
"@id": "#Entrypoint/book",
1729+
"@type": "hydra:Link",
1730+
domain: "#Entrypoint",
1731+
"rdfs:label": "The collection of Book resources",
1732+
"rdfs:range": [
1733+
{ "@id": "hydra:PagedCollection" },
1734+
{
1735+
"owl:equivalentClass": {
1736+
"owl:onProperty": { "@id": "hydra:member" },
1737+
"owl:allValuesFrom": {
1738+
"@id": "http://schema.org/Book",
1739+
},
1740+
},
1741+
},
1742+
],
1743+
},
1744+
"hydra:title": "The collection of Book resources",
1745+
"hydra:readable": true,
1746+
"hydra:writeable": false,
1747+
},
1748+
{
1749+
"@type": "hydra:SupportedProperty",
1750+
"hydra:property": {
1751+
"@id": "#Entrypoint/bookCondition",
1752+
"@type": "hydra:Link",
1753+
domain: "#Entrypoint",
1754+
"rdfs:label": "The collection of BookCondition resources",
1755+
"rdfs:range": [{ "@id": "#BookCondition" }],
1756+
},
1757+
"hydra:title": "The collection of BookCondition resources",
1758+
"hydra:readable": true,
1759+
"hydra:writeable": false,
1760+
},
1761+
],
1762+
"hydra:supportedOperation": {
1763+
"@type": "hydra:Operation",
1764+
"hydra:method": "GET",
1765+
"rdfs:label": "The API entrypoint.",
1766+
returns: "#EntryPoint",
1767+
},
1768+
},
1769+
{
1770+
"@id": "#ConstraintViolation",
1771+
"@type": "hydra:Class",
1772+
"hydra:title": "A constraint violation",
1773+
"hydra:supportedProperty": [],
1774+
},
1775+
{
1776+
"@id": "#ConstraintViolationList",
1777+
"@type": "hydra:Class",
1778+
subClassOf: "hydra:Error",
1779+
"hydra:title": "A constraint violation list",
1780+
"hydra:supportedProperty": [],
1781+
},
1782+
],
1783+
};
1784+
1785+
server.use(
1786+
http.get("http://localhost", () => Response.json(enumEntrypoint, init)),
1787+
http.get("http://localhost/docs.jsonld", () =>
1788+
Response.json(enumDocs, init),
1789+
),
1790+
);
1791+
1792+
const data = await parseHydraDocumentation("http://localhost");
1793+
expect(data.status).toBe(200);
1794+
1795+
const bookConditionResource = data.api.resources?.find(
1796+
(r) => r.id === "http://localhost/docs.jsonld#BookCondition",
1797+
);
1798+
1799+
expect(bookConditionResource).toBeDefined();
1800+
assert(bookConditionResource !== undefined);
1801+
1802+
expect(bookConditionResource.name).toBe("book_conditions");
1803+
expect(bookConditionResource.title).toBe("BookCondition");
1804+
expect(bookConditionResource.url).toBe("http://localhost/book_conditions");
1805+
1806+
// Verify the field was parsed correctly
1807+
assert(bookConditionResource.fields !== null);
1808+
assert(bookConditionResource.fields !== undefined);
1809+
expect(bookConditionResource.fields).toHaveLength(1);
1810+
expect(bookConditionResource.fields[0]?.name).toBe("value");
1811+
expect(bookConditionResource.fields[0]?.range).toBe(
1812+
"http://www.w3.org/2001/XMLSchema#string",
1813+
);
1814+
expect(bookConditionResource.fields[0]?.required).toBe(true);
1815+
1816+
// Readable but not writable (read-only enum)
1817+
expect(bookConditionResource.readableFields).toHaveLength(1);
1818+
expect(bookConditionResource.writableFields).toHaveLength(0);
1819+
1820+
// Verify operations - only GET (item operation from supportedOperation)
1821+
assert(bookConditionResource.operations !== null);
1822+
assert(bookConditionResource.operations !== undefined);
1823+
expect(bookConditionResource.operations).toHaveLength(1);
1824+
expect(bookConditionResource.operations[0]?.method).toBe("GET");
1825+
1826+
// Also verify the book resource still works (Strategy 1 still functions)
1827+
const bookResource = data.api.resources?.find(
1828+
(r) => r.id === "http://schema.org/Book",
1829+
);
1830+
expect(bookResource).toBeDefined();
1831+
expect(bookResource?.name).toBe("books");
1832+
});

src/hydra/parseHydraDocumentation.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,52 @@ function findRelatedClass(
233233
}
234234
}
235235

236+
// Third strategy: For read-only resources, look for rdfs:range with a direct class reference
237+
// This handles enums and other resources that only have GET collection operations
238+
if (Array.isArray(property["http://www.w3.org/2000/01/rdf-schema#range"])) {
239+
for (const range of property["http://www.w3.org/2000/01/rdf-schema#range"]) {
240+
// Check if this range has a direct @id that's not a Hydra core type
241+
if ("@id" in range) {
242+
const rangeId = range["@id"];
243+
if (
244+
rangeId &&
245+
typeof rangeId === "string" &&
246+
rangeId.indexOf("http://www.w3.org/ns/hydra/core") !== 0
247+
) {
248+
try {
249+
return findSupportedClass(docs, rangeId);
250+
} catch {
251+
// Not a valid class, continue to next range
252+
continue;
253+
}
254+
}
255+
}
256+
257+
// Also check if there's an owl:allValuesFrom without strict onProperty checking
258+
// This is a more lenient version of Strategy 1
259+
const equivalentClass =
260+
"http://www.w3.org/2002/07/owl#equivalentClass" in range
261+
? range?.["http://www.w3.org/2002/07/owl#equivalentClass"]?.[0]
262+
: undefined;
263+
264+
if (equivalentClass) {
265+
const allValuesFrom =
266+
equivalentClass["http://www.w3.org/2002/07/owl#allValuesFrom"]?.[0]?.[
267+
"@id"
268+
];
269+
270+
if (allValuesFrom) {
271+
try {
272+
return findSupportedClass(docs, allValuesFrom);
273+
} catch {
274+
// Not a valid class, continue to next range
275+
continue;
276+
}
277+
}
278+
}
279+
}
280+
}
281+
236282
throw new Error(`Cannot find the class related to ${property["@id"]}.`);
237283
}
238284

0 commit comments

Comments
 (0)