Skip to content

Commit e05d67c

Browse files
authored
feat(core): implement exploreDirectory method (#1186)
* feat(content-serdes): add application/ld+json to supported Content-Types This is required for fully supporting the `exploreDirectory` method. See https://www.w3.org/TR/wot-discovery/#exploration-directory-api-things-listing * feat(core): implement `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * test: add test for `exploreDirectory` method * fixup! test: add test for `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! test: add test for `exploreDirectory` method * fixup! test: add test for `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * refactor(core): use validation functions for requestThingDescription * fixup! test: add test for `exploreDirectory` method * fixup! test: add test for `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method * fixup! test: add test for `exploreDirectory` method * fixup! feat(core): implement `exploreDirectory` method
1 parent a70e8b2 commit e05d67c

4 files changed

Lines changed: 302 additions & 8 deletions

File tree

packages/core/src/content-serdes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export class ContentSerdes {
6363
this.instance.addCodec(new JsonCodec(), true);
6464
this.instance.addCodec(new JsonCodec("application/senml+json"));
6565
this.instance.addCodec(new JsonCodec("application/td+json"));
66+
this.instance.addCodec(new JsonCodec("application/ld+json"));
6667
// CBOR
6768
this.instance.addCodec(new CborCodec(), true);
6869
// Text

packages/core/src/validation.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/********************************************************************************
2+
* Copyright (c) 2023 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
* Document License (2015-05-13) which is available at
11+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
14+
********************************************************************************/
15+
16+
import { ErrorObject } from "ajv";
17+
import Helpers from "./helpers";
18+
19+
export function isThingDescription(input: unknown): input is WoT.ThingDescription {
20+
return Helpers.tsSchemaValidator(input);
21+
}
22+
23+
export function getLastValidationErrors() {
24+
const errors = Helpers.tsSchemaValidator.errors?.map((o: ErrorObject) => o.message).join("\n");
25+
return new Error(errors);
26+
}

packages/core/src/wot-impl.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,50 @@ import ConsumedThing from "./consumed-thing";
2121
import Helpers from "./helpers";
2222
import { createLoggers } from "./logger";
2323
import ContentManager from "./content-serdes";
24-
import { ErrorObject } from "ajv";
24+
import { getLastValidationErrors, isThingDescription } from "./validation";
2525

2626
const { debug } = createLoggers("core", "wot-impl");
2727

28+
class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess {
29+
constructor(rawThingDescriptions: WoT.DataSchemaValue, filter?: WoT.ThingFilter) {
30+
this.filter = filter;
31+
this.done = false;
32+
this.rawThingDescriptions = rawThingDescriptions;
33+
}
34+
35+
rawThingDescriptions: WoT.DataSchemaValue;
36+
37+
filter?: WoT.ThingFilter | undefined;
38+
done: boolean;
39+
error?: Error | undefined;
40+
async stop(): Promise<void> {
41+
this.done = true;
42+
}
43+
44+
async *[Symbol.asyncIterator](): AsyncIterator<WoT.ThingDescription> {
45+
if (!(this.rawThingDescriptions instanceof Array)) {
46+
this.error = new Error("Encountered an invalid output value.");
47+
this.done = true;
48+
return;
49+
}
50+
51+
for (const outputValue of this.rawThingDescriptions) {
52+
if (this.done) {
53+
return;
54+
}
55+
56+
if (!isThingDescription(outputValue)) {
57+
this.error = getLastValidationErrors();
58+
continue;
59+
}
60+
61+
yield outputValue;
62+
}
63+
64+
this.done = true;
65+
}
66+
}
67+
2868
export default class WoTImpl {
2969
private srv: Servient;
3070
constructor(srv: Servient) {
@@ -38,7 +78,13 @@ export default class WoTImpl {
3878

3979
/** @inheritDoc */
4080
async exploreDirectory(url: string, filter?: WoT.ThingFilter): Promise<WoT.ThingDiscoveryProcess> {
41-
throw new Error("not implemented");
81+
const directoyThingDescription = await this.requestThingDescription(url);
82+
const consumedDirectoy = await this.consume(directoyThingDescription);
83+
84+
const thingsPropertyOutput = await consumedDirectoy.readProperty("things");
85+
const rawThingDescriptions = await thingsPropertyOutput.value();
86+
87+
return new ThingDiscoveryProcess(rawThingDescriptions, filter);
4288
}
4389

4490
/** @inheritDoc */
@@ -48,14 +94,11 @@ export default class WoTImpl {
4894
const content = await client.requestThingDescription(url);
4995
const value = ContentManager.contentToValue({ type: content.type, body: await content.toBuffer() }, {});
5096

51-
const isValidThingDescription = Helpers.tsSchemaValidator(value);
52-
53-
if (!isValidThingDescription) {
54-
const errors = Helpers.tsSchemaValidator.errors?.map((o: ErrorObject) => o.message).join("\n");
55-
throw new Error(errors);
97+
if (isThingDescription(value)) {
98+
return value;
5699
}
57100

58-
return value as WoT.ThingDescription;
101+
throw getLastValidationErrors();
59102
}
60103

61104
/** @inheritDoc */
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/********************************************************************************
2+
* Copyright (c) 2023 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
* Document License (2015-05-13) which is available at
11+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
14+
********************************************************************************/
15+
16+
import { Form, SecurityScheme } from "@node-wot/td-tools";
17+
import { Subscription } from "rxjs/Subscription";
18+
import { Content } from "../src/content";
19+
import { createLoggers } from "../src/logger";
20+
import { ProtocolClient, ProtocolClientFactory } from "../src/protocol-interfaces";
21+
import Servient from "../src/servient";
22+
import { Readable } from "stream";
23+
import { expect } from "chai";
24+
25+
const { debug, error } = createLoggers("core", "DiscoveryTest");
26+
27+
function createDirectoryTestTd(title: string, thingsPropertyHref: string) {
28+
return {
29+
"@context": "https://www.w3.org/2022/wot/td/v1.1",
30+
title,
31+
security: "nosec_sc",
32+
securityDefinitions: {
33+
nosec_sc: {
34+
scheme: "nosec",
35+
},
36+
},
37+
properties: {
38+
things: {
39+
forms: [
40+
{
41+
href: thingsPropertyHref,
42+
},
43+
],
44+
},
45+
},
46+
};
47+
}
48+
49+
function createDiscoveryContent(td: unknown, contentType: string) {
50+
const buffer = Buffer.from(JSON.stringify(td));
51+
const content = new Content(contentType, Readable.from(buffer));
52+
return content;
53+
}
54+
55+
const directoryTdUrl1 = "test://localhost/valid-output-tds";
56+
const directoryTdUrl2 = "test://localhost/invalid-output-tds";
57+
const directoryTdUrl3 = "test://localhost/no-array-output";
58+
59+
const directoryTdTitle1 = "Directory Test TD 1";
60+
const directoryTdTitle2 = "Directory Test TD 2";
61+
const directoryTdTitle3 = "Directory Test TD 3";
62+
63+
const directoryThingsUrl1 = "test://localhost/things1";
64+
const directoryThingsUrl2 = "test://localhost/things2";
65+
const directoryThingsUrl3 = "test://localhost/things3";
66+
67+
const directoryThingDescription1 = createDirectoryTestTd(directoryTdTitle1, directoryThingsUrl1);
68+
const directoryThingDescription2 = createDirectoryTestTd(directoryTdTitle2, directoryThingsUrl2);
69+
const directoryThingDescription3 = createDirectoryTestTd(directoryTdTitle3, directoryThingsUrl3);
70+
71+
class TestProtocolClient implements ProtocolClient {
72+
async readResource(form: Form): Promise<Content> {
73+
const href = form.href;
74+
75+
switch (href) {
76+
case directoryThingsUrl1:
77+
return createDiscoveryContent([directoryThingDescription1], "application/ld+json");
78+
case directoryThingsUrl2:
79+
return createDiscoveryContent(["I am an invalid TD!"], "application/ld+json");
80+
case directoryThingsUrl3:
81+
return createDiscoveryContent("I am no array and therefore invalid!", "application/ld+json");
82+
}
83+
84+
throw new Error("Invalid URL");
85+
}
86+
87+
writeResource(form: Form, content: Content): Promise<void> {
88+
throw new Error("Method not implemented.");
89+
}
90+
91+
invokeResource(form: Form, content?: Content | undefined): Promise<Content> {
92+
throw new Error("Method not implemented.");
93+
}
94+
95+
unlinkResource(form: Form): Promise<void> {
96+
throw new Error("Method not implemented.");
97+
}
98+
99+
subscribeResource(
100+
form: Form,
101+
next: (content: Content) => void,
102+
error?: ((error: Error) => void) | undefined,
103+
complete?: (() => void) | undefined
104+
): Promise<Subscription> {
105+
throw new Error("Method not implemented.");
106+
}
107+
108+
async requestThingDescription(uri: string): Promise<Content> {
109+
switch (uri) {
110+
case directoryTdUrl1:
111+
debug(`Found corrent URL ${uri} to fetch directory TD`);
112+
return createDiscoveryContent(directoryThingDescription1, "application/td+json");
113+
case directoryTdUrl2:
114+
debug(`Found corrent URL ${uri} to fetch directory TD`);
115+
return createDiscoveryContent(directoryThingDescription2, "application/td+json");
116+
case directoryTdUrl3:
117+
debug(`Found corrent URL ${uri} to fetch directory TD`);
118+
return createDiscoveryContent(directoryThingDescription3, "application/td+json");
119+
}
120+
121+
throw Error("Invalid URL");
122+
}
123+
124+
async start(): Promise<void> {
125+
// Do nothing
126+
}
127+
128+
async stop(): Promise<void> {
129+
// Do nothing
130+
}
131+
132+
setSecurity(metadata: SecurityScheme[], credentials?: unknown): boolean {
133+
return true;
134+
}
135+
}
136+
137+
class TestProtocolClientFactory implements ProtocolClientFactory {
138+
public scheme = "test";
139+
140+
getClient(): ProtocolClient {
141+
return new TestProtocolClient();
142+
}
143+
144+
init(): boolean {
145+
return true;
146+
}
147+
148+
destroy(): boolean {
149+
return true;
150+
}
151+
}
152+
153+
describe("Discovery Tests", () => {
154+
it("should be possible to use the exploreDirectory method", async () => {
155+
const servient = new Servient();
156+
servient.addClientFactory(new TestProtocolClientFactory());
157+
158+
const WoT = await servient.start();
159+
160+
const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl1);
161+
162+
let tdCounter = 0;
163+
for await (const thingDescription of discoveryProcess) {
164+
expect(thingDescription.title).to.eql(directoryTdTitle1);
165+
tdCounter++;
166+
}
167+
expect(tdCounter).to.eql(1);
168+
expect(discoveryProcess.error).to.eq(undefined);
169+
});
170+
171+
it("should receive no output and an error by the exploreDirectory method for invalid returned TDs", async () => {
172+
const servient = new Servient();
173+
servient.addClientFactory(new TestProtocolClientFactory());
174+
175+
const WoT = await servient.start();
176+
177+
const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl2);
178+
179+
let tdCounter = 0;
180+
for await (const thingDescription of discoveryProcess) {
181+
error(`Encountered unexpected TD with title ${thingDescription.title}`);
182+
tdCounter++;
183+
}
184+
expect(tdCounter).to.eql(0);
185+
expect(discoveryProcess.error).to.not.eq(undefined);
186+
});
187+
188+
it("should receive no output and an error by the exploreDirectory method if no array is returned", async () => {
189+
const servient = new Servient();
190+
servient.addClientFactory(new TestProtocolClientFactory());
191+
192+
const WoT = await servient.start();
193+
194+
const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl3);
195+
196+
let tdCounter = 0;
197+
for await (const thingDescription of discoveryProcess) {
198+
error(`Encountered unexpected TD with title ${thingDescription.title}`);
199+
tdCounter++;
200+
}
201+
expect(tdCounter).to.eql(0);
202+
expect(discoveryProcess.error).to.not.eq(undefined);
203+
});
204+
205+
it("should be possible to stop discovery with exploreDirectory prematurely", async () => {
206+
const servient = new Servient();
207+
servient.addClientFactory(new TestProtocolClientFactory());
208+
209+
const WoT = await servient.start();
210+
211+
const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl1);
212+
expect(discoveryProcess.done).to.not.eq(true);
213+
discoveryProcess.stop();
214+
expect(discoveryProcess.done).to.eq(true);
215+
216+
let tdCounter = 0;
217+
for await (const thingDescription of discoveryProcess) {
218+
error(`Encountered unexpected TD with title ${thingDescription.title}`);
219+
tdCounter++;
220+
}
221+
expect(tdCounter).to.eql(0);
222+
expect(discoveryProcess.error).to.eq(undefined);
223+
});
224+
});

0 commit comments

Comments
 (0)