Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): implement exploreDirectory method #1186

Merged
merged 21 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bfcf85c
feat(content-serdes): add application/ld+json to supported Content-Types
JKRhb Dec 7, 2023
77b916f
feat(core): implement `exploreDirectory` method
JKRhb Dec 7, 2023
e9c4881
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 14, 2023
4a50219
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
455af97
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
9faa9d2
test: add test for `exploreDirectory` method
JKRhb Dec 15, 2023
db19eae
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 15, 2023
7eddb7f
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
764d9fa
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
0514567
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
57affb8
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
951ca10
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
cddd356
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
99e102b
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
6f11544
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
568ebcc
refactor(core): use validation functions for requestThingDescription
JKRhb Dec 21, 2023
5e17c5f
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
a06e024
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
66104cf
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
c8fc93f
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
7cc777f
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Comment on lines +23 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion but the term might convey something which is not always the case. It is correct, it is the "last" validation. Anyhow, if used in some other code parts it might report errors of another validation step (not necessarily the one from last isThingDescription) that used Helpers.tsSchemaValidator..

Do you know what I mean.

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 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 @@

/** @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 @@
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();

Check warning on line 101 in packages/core/src/wot-impl.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/wot-impl.ts#L101

Added line #L101 was not covered by tests
}

/** @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);
});
});
Loading