Skip to content

Commit f88a5e7

Browse files
committed
feat: add support for hydra:manages
1 parent 1f6772b commit f88a5e7

File tree

4 files changed

+226
-1
lines changed

4 files changed

+226
-1
lines changed

src/core/Resource.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import type { Operation } from "./Operation.js";
33
import type { Parameter } from "./Parameter.js";
44
import type { Nullable } from "./types.js";
55

6+
export interface ManagesBlock {
7+
property?: string;
8+
object?: string;
9+
}
10+
611
export interface ResourceOptions
712
extends Nullable<{
813
id?: string;
@@ -15,6 +20,7 @@ export interface ResourceOptions
1520
operations?: Operation[];
1621
deprecated?: boolean;
1722
parameters?: Parameter[];
23+
manages?: ManagesBlock[];
1824
}> {}
1925

2026
export class Resource implements ResourceOptions {
@@ -31,6 +37,7 @@ export class Resource implements ResourceOptions {
3137
operations?: Operation[] | null;
3238
deprecated?: boolean | null;
3339
parameters?: Parameter[] | null;
40+
manages?: ManagesBlock[] | null;
3441

3542
constructor(name: string, url: string, options: ResourceOptions = {}) {
3643
this.name = name;

src/hydra/parseHydraDocumentation.test.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2583,3 +2583,172 @@ test("parse a Hydra documentation with bare Link @type (without hydra prefix)",
25832583
expect(reviewField.reference).toBe(reviewResource);
25842584
expect(reviewField.embedded).toBeNull();
25852585
});
2586+
2587+
test("parse a Hydra documentation with hydra:manages", async () => {
2588+
const managesEntrypoint = {
2589+
"@context": {
2590+
"@vocab": "http://localhost/docs.jsonld#",
2591+
hydra: "http://www.w3.org/ns/hydra/core#",
2592+
comment: {
2593+
"@id": "Entrypoint/comment",
2594+
"@type": "@id",
2595+
},
2596+
},
2597+
"@id": "/",
2598+
"@type": "Entrypoint",
2599+
comment: "/comments",
2600+
};
2601+
2602+
const managesDocs = {
2603+
"@context": {
2604+
"@vocab": "http://localhost/docs.jsonld#",
2605+
hydra: "http://www.w3.org/ns/hydra/core#",
2606+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
2607+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
2608+
xmls: "http://www.w3.org/2001/XMLSchema#",
2609+
owl: "http://www.w3.org/2002/07/owl#",
2610+
domain: {
2611+
"@id": "rdfs:domain",
2612+
"@type": "@id",
2613+
},
2614+
range: {
2615+
"@id": "rdfs:range",
2616+
"@type": "@id",
2617+
},
2618+
expects: {
2619+
"@id": "hydra:expects",
2620+
"@type": "@id",
2621+
},
2622+
returns: {
2623+
"@id": "hydra:returns",
2624+
"@type": "@id",
2625+
},
2626+
},
2627+
"@id": "/docs.jsonld",
2628+
"hydra:title": "API with manages",
2629+
"hydra:description": "A test",
2630+
"hydra:entrypoint": "/",
2631+
"hydra:supportedClass": [
2632+
{
2633+
"@id": "http://schema.org/Comment",
2634+
"@type": "hydra:Class",
2635+
"rdfs:label": "Comment",
2636+
"hydra:title": "Comment",
2637+
"hydra:supportedProperty": [
2638+
{
2639+
"@type": "hydra:SupportedProperty",
2640+
"hydra:property": {
2641+
"@id": "http://schema.org/text",
2642+
"@type": "rdf:Property",
2643+
"rdfs:label": "text",
2644+
domain: "http://schema.org/Comment",
2645+
range: "xmls:string",
2646+
},
2647+
"hydra:title": "text",
2648+
"hydra:required": true,
2649+
"hydra:readable": true,
2650+
"hydra:writeable": true,
2651+
},
2652+
{
2653+
"@type": "hydra:SupportedProperty",
2654+
"hydra:property": {
2655+
"@id": "http://schema.org/about",
2656+
"@type": "hydra:Link",
2657+
"rdfs:label": "about",
2658+
domain: "http://schema.org/Comment",
2659+
range: "http://schema.org/Thing",
2660+
},
2661+
"hydra:title": "about",
2662+
"hydra:required": true,
2663+
"hydra:readable": true,
2664+
"hydra:writeable": true,
2665+
},
2666+
],
2667+
"hydra:supportedOperation": [
2668+
{
2669+
"@type": "hydra:Operation",
2670+
"hydra:method": "GET",
2671+
"hydra:title": "Retrieves Comment resource.",
2672+
"rdfs:label": "Retrieves Comment resource.",
2673+
returns: "http://schema.org/Comment",
2674+
},
2675+
],
2676+
},
2677+
{
2678+
"@id": "#Entrypoint",
2679+
"@type": "hydra:Class",
2680+
"hydra:title": "The API entrypoint",
2681+
"hydra:supportedProperty": [
2682+
{
2683+
"@type": "hydra:SupportedProperty",
2684+
"hydra:property": {
2685+
"@id": "#Entrypoint/comment",
2686+
"@type": "hydra:Link",
2687+
domain: "#Entrypoint",
2688+
"rdfs:label": "The collection of Comment resources",
2689+
"rdfs:range": [
2690+
{ "@id": "hydra:Collection" },
2691+
{
2692+
"owl:equivalentClass": {
2693+
"owl:onProperty": { "@id": "hydra:member" },
2694+
"owl:allValuesFrom": { "@id": "http://schema.org/Comment" },
2695+
},
2696+
},
2697+
],
2698+
"hydra:manages": [
2699+
{
2700+
"hydra:property": { "@id": "http://example.com/vocab#comment" },
2701+
"hydra:object": { "@id": "http://schema.org/Comment" },
2702+
},
2703+
],
2704+
},
2705+
"hydra:title": "The collection of Comment resources",
2706+
"hydra:readable": true,
2707+
"hydra:writeable": false,
2708+
"hydra:supportedOperation": [
2709+
{
2710+
"@type": "hydra:Operation",
2711+
"hydra:method": "GET",
2712+
"hydra:title": "Retrieves the collection of Comment resources.",
2713+
"rdfs:label": "Retrieves the collection of Comment resources.",
2714+
returns: "hydra:Collection",
2715+
},
2716+
],
2717+
},
2718+
],
2719+
},
2720+
],
2721+
};
2722+
2723+
const init = { headers: { "Content-Type": "application/ld+json" } };
2724+
server.use(
2725+
http.get("http://localhost", () =>
2726+
Response.json(managesEntrypoint, {
2727+
headers: {
2728+
...init.headers,
2729+
Link: '<http://localhost/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"',
2730+
},
2731+
}),
2732+
),
2733+
http.get("http://localhost/docs.jsonld", () =>
2734+
Response.json(managesDocs, init),
2735+
),
2736+
);
2737+
2738+
const data = await parseHydraDocumentation("http://localhost");
2739+
expect(data.status).toBe(200);
2740+
2741+
const commentResource = data.api.resources?.find(
2742+
(r) => r.id === "http://schema.org/Comment",
2743+
);
2744+
expect(commentResource).toBeDefined();
2745+
assert(commentResource !== undefined);
2746+
2747+
expect(commentResource.manages).toBeDefined();
2748+
expect(commentResource.manages).toHaveLength(1);
2749+
expect(commentResource.manages?.[0]).toEqual({
2750+
property: "http://example.com/vocab#comment",
2751+
object: "http://schema.org/Comment",
2752+
});
2753+
});
2754+

src/hydra/parseHydraDocumentation.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import jsonld from "jsonld";
22
import type { OperationType, Parameter } from "../core/index.js";
33
import { Api, Field, Operation, Resource } from "../core/index.js";
4+
import type { ManagesBlock } from "../core/Resource.js";
45
import type { RequestInitExtended } from "../core/types.js";
56
import { removeTrailingSlash } from "../core/utils/index.js";
67
import fetchJsonLd from "./fetchJsonLd.js";
@@ -183,7 +184,8 @@ function findRelatedClass(
183184
docs: ExpandedDoc[],
184185
property: ExpandedRdfProperty,
185186
): ExpandedClass {
186-
// Use the entrypoint property's owl:equivalentClass if available
187+
// Try to use hydra:manages if available (new approach)
188+
// Otherwise fall back to owl:equivalentClass (legacy approach)
187189

188190
for (const range of property["http://www.w3.org/2000/01/rdf-schema#range"] ??
189191
[]) {
@@ -289,6 +291,36 @@ function findRelatedClass(
289291
throw new Error(`Cannot find the class related to ${property["@id"]}.`);
290292
}
291293

294+
/**
295+
* Extracts manages blocks from a property.
296+
* A manages block describes the relations between collection members and other resources.
297+
* @param {ExpandedRdfProperty} property The property containing manages blocks.
298+
* @returns {ManagesBlock[]} Array of manages blocks.
299+
*/
300+
function getManagesBlocks(property: ExpandedRdfProperty): ManagesBlock[] {
301+
const manages = property["http://www.w3.org/ns/hydra/core#manages"];
302+
303+
if (!manages || !Array.isArray(manages)) {
304+
return [];
305+
}
306+
307+
return manages
308+
.map((manage) => {
309+
const prop = manage["http://www.w3.org/ns/hydra/core#property"]?.[0]?.["@id"];
310+
const object = manage["http://www.w3.org/ns/hydra/core#object"]?.[0]?.["@id"];
311+
312+
if (!prop && !object) {
313+
return null;
314+
}
315+
316+
return {
317+
...(prop && { property: prop }),
318+
...(object && { object }),
319+
} as ManagesBlock;
320+
})
321+
.filter((block): block is ManagesBlock => block !== null);
322+
}
323+
292324
/**
293325
* Parses Hydra documentation and converts it to an intermediate representation.
294326
* @param {string} entrypointUrl The API entrypoint URL.
@@ -530,6 +562,8 @@ export default async function parseHydraDocumentation(
530562
operations.push(operation);
531563
}
532564

565+
const manages = getManagesBlocks(property);
566+
533567
const resource = new Resource(guessNameFromUrl(url, entrypointUrl), url, {
534568
id: relatedClass["@id"],
535569
title:
@@ -544,6 +578,7 @@ export default async function parseHydraDocumentation(
544578
relatedClass?.["http://www.w3.org/2002/07/owl#deprecated"]?.[0]?.[
545579
"@value"
546580
] ?? false,
581+
...(manages.length > 0 && { manages }),
547582
});
548583

549584
resource.parameters = [];

src/hydra/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ export interface IriTemplateMapping {
55
required: boolean;
66
}
77

8+
export interface ExpandedManages {
9+
"http://www.w3.org/ns/hydra/core#property"?: [
10+
{
11+
"@id": string;
12+
},
13+
];
14+
"http://www.w3.org/ns/hydra/core#object"?: [
15+
{
16+
"@id": string;
17+
},
18+
];
19+
}
20+
821
export interface ExpandedOperation {
922
"@type": ["http://www.w3.org/ns/hydra/core#Operation"];
1023
"http://www.w3.org/2000/01/rdf-schema#label": [
@@ -88,6 +101,7 @@ export interface ExpandedRdfProperty {
88101
"@value": number;
89102
},
90103
];
104+
"http://www.w3.org/ns/hydra/core#manages"?: ExpandedManages[];
91105
}
92106

93107
interface ExpandedSupportedProperty {

0 commit comments

Comments
 (0)