Skip to content

Commit

Permalink
feat(core): implement exploreDirectory method (eclipse-thingweb#1186)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
JKRhb authored Dec 21, 2023
1 parent a70e8b2 commit e05d67c
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/core/src/content-serdes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class ContentSerdes {
this.instance.addCodec(new JsonCodec(), true);
this.instance.addCodec(new JsonCodec("application/senml+json"));
this.instance.addCodec(new JsonCodec("application/td+json"));
this.instance.addCodec(new JsonCodec("application/ld+json"));
// CBOR
this.instance.addCodec(new CborCodec(), true);
// Text
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/********************************************************************************
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
* Document License (2015-05-13) which is available at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/

import { ErrorObject } from "ajv";
import Helpers from "./helpers";

export function isThingDescription(input: unknown): input is WoT.ThingDescription {
return Helpers.tsSchemaValidator(input);
}

export function getLastValidationErrors() {
const errors = Helpers.tsSchemaValidator.errors?.map((o: ErrorObject) => o.message).join("\n");
return new Error(errors);
}
59 changes: 51 additions & 8 deletions packages/core/src/wot-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,50 @@ import ConsumedThing from "./consumed-thing";
import Helpers from "./helpers";
import { createLoggers } from "./logger";
import ContentManager from "./content-serdes";
import { ErrorObject } from "ajv";
import { getLastValidationErrors, isThingDescription } from "./validation";

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

class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess {
constructor(rawThingDescriptions: WoT.DataSchemaValue, filter?: WoT.ThingFilter) {
this.filter = filter;
this.done = false;
this.rawThingDescriptions = rawThingDescriptions;
}

rawThingDescriptions: WoT.DataSchemaValue;

filter?: WoT.ThingFilter | undefined;
done: boolean;
error?: Error | undefined;
async stop(): Promise<void> {
this.done = true;
}

async *[Symbol.asyncIterator](): AsyncIterator<WoT.ThingDescription> {
if (!(this.rawThingDescriptions instanceof Array)) {
this.error = new Error("Encountered an invalid output value.");
this.done = true;
return;
}

for (const outputValue of this.rawThingDescriptions) {
if (this.done) {
return;
}

if (!isThingDescription(outputValue)) {
this.error = getLastValidationErrors();
continue;
}

yield outputValue;
}

this.done = true;
}
}

export default class WoTImpl {
private srv: Servient;
constructor(srv: Servient) {
Expand All @@ -38,7 +78,13 @@ export default class WoTImpl {

/** @inheritDoc */
async exploreDirectory(url: string, filter?: WoT.ThingFilter): Promise<WoT.ThingDiscoveryProcess> {
throw new Error("not implemented");
const directoyThingDescription = await this.requestThingDescription(url);
const consumedDirectoy = await this.consume(directoyThingDescription);

const thingsPropertyOutput = await consumedDirectoy.readProperty("things");
const rawThingDescriptions = await thingsPropertyOutput.value();

return new ThingDiscoveryProcess(rawThingDescriptions, filter);
}

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

const isValidThingDescription = Helpers.tsSchemaValidator(value);

if (!isValidThingDescription) {
const errors = Helpers.tsSchemaValidator.errors?.map((o: ErrorObject) => o.message).join("\n");
throw new Error(errors);
if (isThingDescription(value)) {
return value;
}

return value as WoT.ThingDescription;
throw getLastValidationErrors();
}

/** @inheritDoc */
Expand Down
224 changes: 224 additions & 0 deletions packages/core/test/DiscoveryTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/********************************************************************************
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
* Document License (2015-05-13) which is available at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/

import { Form, SecurityScheme } from "@node-wot/td-tools";
import { Subscription } from "rxjs/Subscription";
import { Content } from "../src/content";
import { createLoggers } from "../src/logger";
import { ProtocolClient, ProtocolClientFactory } from "../src/protocol-interfaces";
import Servient from "../src/servient";
import { Readable } from "stream";
import { expect } from "chai";

const { debug, error } = createLoggers("core", "DiscoveryTest");

function createDirectoryTestTd(title: string, thingsPropertyHref: string) {
return {
"@context": "https://www.w3.org/2022/wot/td/v1.1",
title,
security: "nosec_sc",
securityDefinitions: {
nosec_sc: {
scheme: "nosec",
},
},
properties: {
things: {
forms: [
{
href: thingsPropertyHref,
},
],
},
},
};
}

function createDiscoveryContent(td: unknown, contentType: string) {
const buffer = Buffer.from(JSON.stringify(td));
const content = new Content(contentType, Readable.from(buffer));
return content;
}

const directoryTdUrl1 = "test://localhost/valid-output-tds";
const directoryTdUrl2 = "test://localhost/invalid-output-tds";
const directoryTdUrl3 = "test://localhost/no-array-output";

const directoryTdTitle1 = "Directory Test TD 1";
const directoryTdTitle2 = "Directory Test TD 2";
const directoryTdTitle3 = "Directory Test TD 3";

const directoryThingsUrl1 = "test://localhost/things1";
const directoryThingsUrl2 = "test://localhost/things2";
const directoryThingsUrl3 = "test://localhost/things3";

const directoryThingDescription1 = createDirectoryTestTd(directoryTdTitle1, directoryThingsUrl1);
const directoryThingDescription2 = createDirectoryTestTd(directoryTdTitle2, directoryThingsUrl2);
const directoryThingDescription3 = createDirectoryTestTd(directoryTdTitle3, directoryThingsUrl3);

class TestProtocolClient implements ProtocolClient {
async readResource(form: Form): Promise<Content> {
const href = form.href;

switch (href) {
case directoryThingsUrl1:
return createDiscoveryContent([directoryThingDescription1], "application/ld+json");
case directoryThingsUrl2:
return createDiscoveryContent(["I am an invalid TD!"], "application/ld+json");
case directoryThingsUrl3:
return createDiscoveryContent("I am no array and therefore invalid!", "application/ld+json");
}

throw new Error("Invalid URL");
}

writeResource(form: Form, content: Content): Promise<void> {
throw new Error("Method not implemented.");
}

invokeResource(form: Form, content?: Content | undefined): Promise<Content> {
throw new Error("Method not implemented.");
}

unlinkResource(form: Form): Promise<void> {
throw new Error("Method not implemented.");
}

subscribeResource(
form: Form,
next: (content: Content) => void,
error?: ((error: Error) => void) | undefined,
complete?: (() => void) | undefined
): Promise<Subscription> {
throw new Error("Method not implemented.");
}

async requestThingDescription(uri: string): Promise<Content> {
switch (uri) {
case directoryTdUrl1:
debug(`Found corrent URL ${uri} to fetch directory TD`);
return createDiscoveryContent(directoryThingDescription1, "application/td+json");
case directoryTdUrl2:
debug(`Found corrent URL ${uri} to fetch directory TD`);
return createDiscoveryContent(directoryThingDescription2, "application/td+json");
case directoryTdUrl3:
debug(`Found corrent URL ${uri} to fetch directory TD`);
return createDiscoveryContent(directoryThingDescription3, "application/td+json");
}

throw Error("Invalid URL");
}

async start(): Promise<void> {
// Do nothing
}

async stop(): Promise<void> {
// Do nothing
}

setSecurity(metadata: SecurityScheme[], credentials?: unknown): boolean {
return true;
}
}

class TestProtocolClientFactory implements ProtocolClientFactory {
public scheme = "test";

getClient(): ProtocolClient {
return new TestProtocolClient();
}

init(): boolean {
return true;
}

destroy(): boolean {
return true;
}
}

describe("Discovery Tests", () => {
it("should be possible to use the exploreDirectory method", async () => {
const servient = new Servient();
servient.addClientFactory(new TestProtocolClientFactory());

const WoT = await servient.start();

const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl1);

let tdCounter = 0;
for await (const thingDescription of discoveryProcess) {
expect(thingDescription.title).to.eql(directoryTdTitle1);
tdCounter++;
}
expect(tdCounter).to.eql(1);
expect(discoveryProcess.error).to.eq(undefined);
});

it("should receive no output and an error by the exploreDirectory method for invalid returned TDs", async () => {
const servient = new Servient();
servient.addClientFactory(new TestProtocolClientFactory());

const WoT = await servient.start();

const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl2);

let tdCounter = 0;
for await (const thingDescription of discoveryProcess) {
error(`Encountered unexpected TD with title ${thingDescription.title}`);
tdCounter++;
}
expect(tdCounter).to.eql(0);
expect(discoveryProcess.error).to.not.eq(undefined);
});

it("should receive no output and an error by the exploreDirectory method if no array is returned", async () => {
const servient = new Servient();
servient.addClientFactory(new TestProtocolClientFactory());

const WoT = await servient.start();

const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl3);

let tdCounter = 0;
for await (const thingDescription of discoveryProcess) {
error(`Encountered unexpected TD with title ${thingDescription.title}`);
tdCounter++;
}
expect(tdCounter).to.eql(0);
expect(discoveryProcess.error).to.not.eq(undefined);
});

it("should be possible to stop discovery with exploreDirectory prematurely", async () => {
const servient = new Servient();
servient.addClientFactory(new TestProtocolClientFactory());

const WoT = await servient.start();

const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl1);
expect(discoveryProcess.done).to.not.eq(true);
discoveryProcess.stop();
expect(discoveryProcess.done).to.eq(true);

let tdCounter = 0;
for await (const thingDescription of discoveryProcess) {
error(`Encountered unexpected TD with title ${thingDescription.title}`);
tdCounter++;
}
expect(tdCounter).to.eql(0);
expect(discoveryProcess.error).to.eq(undefined);
});
});

0 comments on commit e05d67c

Please sign in to comment.