Skip to content

Commit e9a7b7e

Browse files
authored
fix(hydra): resolve related class for enum and read-only resources (#164)
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 e9a7b7e

File tree

2 files changed

+433
-3
lines changed

2 files changed

+433
-3
lines changed

src/hydra/parseHydraDocumentation.test.ts

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,3 +1599,385 @@ 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+
});
1833+
1834+
test("parse a Hydra documentation with owl:equivalentClass without onProperty hydra:member", async () => {
1835+
const enumEntrypoint = {
1836+
"@context": {
1837+
"@vocab": "http://localhost/docs.jsonld#",
1838+
hydra: "http://www.w3.org/ns/hydra/core#",
1839+
bookCondition: {
1840+
"@id": "Entrypoint/bookCondition",
1841+
"@type": "@id",
1842+
},
1843+
},
1844+
"@id": "/",
1845+
"@type": "Entrypoint",
1846+
bookCondition: "/book_conditions",
1847+
};
1848+
1849+
const enumDocs = {
1850+
"@context": {
1851+
"@vocab": "http://localhost/docs.jsonld#",
1852+
hydra: "http://www.w3.org/ns/hydra/core#",
1853+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
1854+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
1855+
xmls: "http://www.w3.org/2001/XMLSchema#",
1856+
owl: "http://www.w3.org/2002/07/owl#",
1857+
domain: {
1858+
"@id": "rdfs:domain",
1859+
"@type": "@id",
1860+
},
1861+
range: {
1862+
"@id": "rdfs:range",
1863+
"@type": "@id",
1864+
},
1865+
subClassOf: {
1866+
"@id": "rdfs:subClassOf",
1867+
"@type": "@id",
1868+
},
1869+
expects: {
1870+
"@id": "hydra:expects",
1871+
"@type": "@id",
1872+
},
1873+
returns: {
1874+
"@id": "hydra:returns",
1875+
"@type": "@id",
1876+
},
1877+
},
1878+
"@id": "/docs.jsonld",
1879+
"hydra:title": "API with enums",
1880+
"hydra:description": "An API that exposes enum resources",
1881+
"hydra:entrypoint": "/",
1882+
"hydra:supportedClass": [
1883+
{
1884+
"@id": "#BookCondition",
1885+
"@type": "hydra:Class",
1886+
"rdfs:label": "BookCondition",
1887+
"hydra:title": "BookCondition",
1888+
"hydra:supportedProperty": [
1889+
{
1890+
"@type": "hydra:SupportedProperty",
1891+
"hydra:property": {
1892+
"@id": "#BookCondition/value",
1893+
"@type": "rdf:Property",
1894+
"rdfs:label": "value",
1895+
domain: "#BookCondition",
1896+
range: "xmls:string",
1897+
},
1898+
"hydra:title": "value",
1899+
"hydra:required": true,
1900+
"hydra:readable": true,
1901+
"hydra:writeable": false,
1902+
},
1903+
],
1904+
"hydra:supportedOperation": [
1905+
{
1906+
"@type": "hydra:Operation",
1907+
"hydra:method": "GET",
1908+
"hydra:title": "Retrieves BookCondition resource.",
1909+
"rdfs:label": "Retrieves BookCondition resource.",
1910+
returns: "#BookCondition",
1911+
},
1912+
],
1913+
},
1914+
{
1915+
"@id": "#Entrypoint",
1916+
"@type": "hydra:Class",
1917+
"hydra:title": "The API entrypoint",
1918+
"hydra:supportedProperty": [
1919+
{
1920+
"@type": "hydra:SupportedProperty",
1921+
"hydra:property": {
1922+
"@id": "#Entrypoint/bookCondition",
1923+
"@type": "hydra:Link",
1924+
domain: "#Entrypoint",
1925+
"rdfs:label": "The collection of BookCondition resources",
1926+
"rdfs:range": [
1927+
{ "@id": "hydra:Collection" },
1928+
{
1929+
"owl:equivalentClass": {
1930+
"owl:allValuesFrom": {
1931+
"@id": "#BookCondition",
1932+
},
1933+
},
1934+
},
1935+
],
1936+
},
1937+
"hydra:title": "The collection of BookCondition resources",
1938+
"hydra:readable": true,
1939+
"hydra:writeable": false,
1940+
},
1941+
],
1942+
"hydra:supportedOperation": {
1943+
"@type": "hydra:Operation",
1944+
"hydra:method": "GET",
1945+
"rdfs:label": "The API entrypoint.",
1946+
returns: "#EntryPoint",
1947+
},
1948+
},
1949+
{
1950+
"@id": "#ConstraintViolation",
1951+
"@type": "hydra:Class",
1952+
"hydra:title": "A constraint violation",
1953+
"hydra:supportedProperty": [],
1954+
},
1955+
{
1956+
"@id": "#ConstraintViolationList",
1957+
"@type": "hydra:Class",
1958+
subClassOf: "hydra:Error",
1959+
"hydra:title": "A constraint violation list",
1960+
"hydra:supportedProperty": [],
1961+
},
1962+
],
1963+
};
1964+
1965+
server.use(
1966+
http.get("http://localhost", () => Response.json(enumEntrypoint, init)),
1967+
http.get("http://localhost/docs.jsonld", () =>
1968+
Response.json(enumDocs, init),
1969+
),
1970+
);
1971+
1972+
const data = await parseHydraDocumentation("http://localhost");
1973+
expect(data.status).toBe(200);
1974+
1975+
const bookConditionResource = data.api.resources?.find(
1976+
(r) => r.id === "http://localhost/docs.jsonld#BookCondition",
1977+
);
1978+
1979+
expect(bookConditionResource).toBeDefined();
1980+
assert(bookConditionResource !== undefined);
1981+
expect(bookConditionResource.name).toBe("book_conditions");
1982+
expect(bookConditionResource.title).toBe("BookCondition");
1983+
});

0 commit comments

Comments
 (0)