From bfcf85cdcc65581b30768fa159895d4e4b38c295 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 7 Dec 2023 12:00:57 +0100 Subject: [PATCH 01/21] 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 --- packages/core/src/content-serdes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/content-serdes.ts b/packages/core/src/content-serdes.ts index a9165995f..0fe763755 100644 --- a/packages/core/src/content-serdes.ts +++ b/packages/core/src/content-serdes.ts @@ -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 From 77b916f43512028fd872bb0b05ff3cce91266cfe Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 7 Dec 2023 08:49:24 +0100 Subject: [PATCH 02/21] feat(core): implement `exploreDirectory` method --- packages/core/src/wot-impl.ts | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index 42ca61395..bf02f53e2 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -22,9 +22,60 @@ import Helpers from "./helpers"; import { createLoggers } from "./logger"; import ContentManager from "./content-serdes"; import { ErrorObject } from "ajv"; +import { ThingDescription } from "wot-thing-description-types"; const { debug } = createLoggers("core", "wot-impl"); +// @ts-expect-error Typescript currently encounters an error here that *should* be a false positve +class ExploreDirectoryDatasource implements UnderlyingDefaultSource { + constructor(rawThingDescriptions: WoT.DataSchemaValue) { + this.rawThingDescriptions = rawThingDescriptions; + } + + rawThingDescriptions: WoT.DataSchemaValue; + + start(controller: ReadableStreamDefaultController) { + if (!(this.rawThingDescriptions instanceof Array)) { + controller.error(new Error("Encountered an invalid output value.")); + controller.close(); + return; + } + + for (const outputValue of this.rawThingDescriptions) { + // TODO: Add validation + controller.enqueue(outputValue as WoT.ThingDescription); + } + + controller.close(); + } +} + +class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess { + constructor(thingDescriptionStream: ReadableStream, filter?: WoT.ThingFilter) { + this.filter = filter; + this.done = false; + this.thingDescriptionStream = thingDescriptionStream; + } + + thingDescriptionStream: ReadableStream; + + filter?: WoT.ThingFilter | undefined; + done: boolean; + error?: Error | undefined; + async stop(): Promise { + await this.thingDescriptionStream.cancel(); + this.done = true; + } + + async *[Symbol.asyncIterator](): AsyncIterator { + // @ts-expect-error Typescript currently encounters an error here that *should* be a false positve + for await (const thingDescription of this.thingDescriptionStream) { + yield thingDescription; + } + this.done = true; + } +} + export default class WoTImpl { private srv: Servient; constructor(srv: Servient) { @@ -38,7 +89,15 @@ export default class WoTImpl { /** @inheritDoc */ async exploreDirectory(url: string, filter?: WoT.ThingFilter): Promise { - 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(); + const thingDescriptionDataSource = new ExploreDirectoryDatasource(rawThingDescriptions); + + const thingDescriptionStream = new ReadableStream(thingDescriptionDataSource); + return new ThingDiscoveryProcess(thingDescriptionStream, filter); } /** @inheritDoc */ From e9c48815021eb3ca217b30227d16744da5bbaa02 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 14 Dec 2023 12:23:30 +0100 Subject: [PATCH 03/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/wot-impl.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index bf02f53e2..46ee7216d 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -69,9 +69,7 @@ class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess { async *[Symbol.asyncIterator](): AsyncIterator { // @ts-expect-error Typescript currently encounters an error here that *should* be a false positve - for await (const thingDescription of this.thingDescriptionStream) { - yield thingDescription; - } + yield* this.thingDescriptionStream; this.done = true; } } From 4a50219b00ca570abe415bcdb0e0d05b2dbb904b Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 15 Dec 2023 13:48:02 +0100 Subject: [PATCH 04/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/validation.ts | 41 +++++++++++++++++++++++++++++++++ packages/core/src/wot-impl.ts | 20 ++++++++++------ 2 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/validation.ts diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts new file mode 100644 index 000000000..291471454 --- /dev/null +++ b/packages/core/src/validation.ts @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +export function isThingDescription(input: unknown): input is WoT.ThingDescription { + // TODO: Improve validation function + if (input == null || typeof input !== "object") { + return false; + } + + const thingDescription = input as WoT.ThingDescription; + + if (thingDescription["@context"] == null) { + return false; + } + + if (thingDescription.title == null) { + return false; + } + + if (thingDescription.security == null) { + return false; + } + + if (thingDescription.securityDefinitions == null) { + return false; + } + + return true; +} diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index 46ee7216d..fb2e0ccda 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -23,27 +23,33 @@ import { createLoggers } from "./logger"; import ContentManager from "./content-serdes"; import { ErrorObject } from "ajv"; import { ThingDescription } from "wot-thing-description-types"; +import { isThingDescription } from "./validation"; const { debug } = createLoggers("core", "wot-impl"); // @ts-expect-error Typescript currently encounters an error here that *should* be a false positve class ExploreDirectoryDatasource implements UnderlyingDefaultSource { - constructor(rawThingDescriptions: WoT.DataSchemaValue) { - this.rawThingDescriptions = rawThingDescriptions; + constructor(directoryOutput: WoT.DataSchemaValue) { + this.directoryOutput = directoryOutput; } - rawThingDescriptions: WoT.DataSchemaValue; + directoryOutput: WoT.DataSchemaValue; start(controller: ReadableStreamDefaultController) { - if (!(this.rawThingDescriptions instanceof Array)) { + if (!(this.directoryOutput instanceof Array)) { controller.error(new Error("Encountered an invalid output value.")); controller.close(); return; } - for (const outputValue of this.rawThingDescriptions) { - // TODO: Add validation - controller.enqueue(outputValue as WoT.ThingDescription); + for (const outputValue of this.directoryOutput) { + if (!isThingDescription(outputValue)) { + const validationError = new Error("Validation of Thing Description failed"); + controller.error(validationError); + continue; + } + + controller.enqueue(outputValue); } controller.close(); From 455af97d41d4c9d1306142b25832c896eb566811 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 15 Dec 2023 17:24:15 +0100 Subject: [PATCH 05/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/helpers.ts | 14 +++-------- packages/core/src/validation.ts | 43 ++++++++++++++++----------------- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 1dfd7e84b..24b46c85c 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -40,21 +40,13 @@ import { ThingInteraction, ThingModelHelpers } from "@node-wot/td-tools"; import { Resolver } from "@node-wot/td-tools/src/resolver-interface"; import { PropertyElement, DataSchema } from "wot-thing-description-types"; import { createLoggers } from "./logger"; +import { createWotAjvInstance } from "./validation"; const { debug, error, warn } = createLoggers("core", "helpers"); const tdSchema = TDSchema; -// RegExps take from https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts -const ajv = new Ajv({ strict: false }) - .addFormat( - "iri-reference", - /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i - ) - .addFormat("uri", /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/) - .addFormat( - "date-time", - /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/ - ); +const ajv = createWotAjvInstance(); + export default class Helpers implements Resolver { static tsSchemaValidator = ajv.compile(Helpers.createExposeThingInitSchema(tdSchema)) as ValidateFunction; diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index 291471454..6a31cbc4e 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -13,29 +13,28 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -export function isThingDescription(input: unknown): input is WoT.ThingDescription { - // TODO: Improve validation function - if (input == null || typeof input !== "object") { - return false; - } - - const thingDescription = input as WoT.ThingDescription; - - if (thingDescription["@context"] == null) { - return false; - } +import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json"; +import Ajv, { ValidateFunction } from "ajv"; - if (thingDescription.title == null) { - return false; - } - - if (thingDescription.security == null) { - return false; - } +export function createWotAjvInstance() { + return ( + new Ajv({ strict: false }) + // RegExps taken from https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts + .addFormat( + "iri-reference", + /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i + ) + .addFormat("uri", /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/) + .addFormat( + "date-time", + /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/ + ) + ); +} - if (thingDescription.securityDefinitions == null) { - return false; - } +const ajv = createWotAjvInstance(); +const validateTd = ajv.compile(TDSchema) as ValidateFunction; - return true; +export function isThingDescription(input: unknown): input is WoT.ThingDescription { + return validateTd(input); } From 9faa9d21944098395654edbf3dbc7472da761695 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 15 Dec 2023 17:25:11 +0100 Subject: [PATCH 06/21] test: add test for `exploreDirectory` method --- packages/core/test/DiscoveryTest.ts | 131 ++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 packages/core/test/DiscoveryTest.ts diff --git a/packages/core/test/DiscoveryTest.ts b/packages/core/test/DiscoveryTest.ts new file mode 100644 index 000000000..9f2bdeb00 --- /dev/null +++ b/packages/core/test/DiscoveryTest.ts @@ -0,0 +1,131 @@ +/******************************************************************************** + * 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 } = createLoggers("core", "DiscoveryTest"); + +const directoryTdUrl = "test://localhost/.well-known/wot"; +const directoryThingsUrl = "test://localhost/things"; + +const directoryThingDescription = { + "@context": "https://www.w3.org/2022/wot/td/v1.1", + title: "Directory Test TD", + security: "nosec_sc", + securityDefinitions: { + "nosec_sc": { + scheme: "nosec", + } + }, + properties: { + things: { + forms: [{ + href: directoryThingsUrl, + }] + } + } +} + + +class TestProtocolClient implements ProtocolClient { + async readResource(form: Form): Promise { + if (form.href === directoryThingsUrl) { + const buffer = Buffer.from(JSON.stringify([directoryThingDescription])); + const content = new Content("application/ld+json", Readable.from(buffer)); + return content; + } + + throw new Error("Invalid URL"); + } + + writeResource(form: Form, content: Content): Promise { + throw new Error("Method not implemented."); + } + + invokeResource(form: Form, content?: Content | undefined): Promise { + throw new Error("Method not implemented."); + } + + unlinkResource(form: Form): Promise { + throw new Error("Method not implemented."); + } + + subscribeResource(form: Form, next: (content: Content) => void, error?: ((error: Error) => void) | undefined, complete?: (() => void) | undefined): Promise { + throw new Error("Method not implemented."); + } + + async requestThingDescription(uri: string): Promise { + if (uri === directoryTdUrl) { + debug(`Found corrent URL ${directoryTdUrl} to fetch directory TD`); + const buffer = Buffer.from(JSON.stringify(directoryThingDescription)); + const content = new Content("application/td+json", Readable.from(buffer)); + return content; + } + + throw Error("Invalid URL"); + } + + async start(): Promise { + // Do nothing + } + + async stop(): Promise { + // 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(directoryTdUrl); + + for await (const thingDescription of discoveryProcess) { + expect(thingDescription.title === "Directory Test TD"); + } + }); +}); From db19eaea69e9a1687a8c89ce72b79f94529d8318 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 15 Dec 2023 17:26:01 +0100 Subject: [PATCH 07/21] fixup! test: add test for `exploreDirectory` method --- packages/core/test/DiscoveryTest.ts | 44 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/core/test/DiscoveryTest.ts b/packages/core/test/DiscoveryTest.ts index 9f2bdeb00..cb3d1ad4f 100644 --- a/packages/core/test/DiscoveryTest.ts +++ b/packages/core/test/DiscoveryTest.ts @@ -32,22 +32,23 @@ const directoryThingDescription = { title: "Directory Test TD", security: "nosec_sc", securityDefinitions: { - "nosec_sc": { + nosec_sc: { scheme: "nosec", - } + }, }, properties: { things: { - forms: [{ - href: directoryThingsUrl, - }] - } - } -} - + forms: [ + { + href: directoryThingsUrl, + }, + ], + }, + }, +}; class TestProtocolClient implements ProtocolClient { - async readResource(form: Form): Promise { + async readResource(form: Form): Promise { if (form.href === directoryThingsUrl) { const buffer = Buffer.from(JSON.stringify([directoryThingDescription])); const content = new Content("application/ld+json", Readable.from(buffer)); @@ -69,7 +70,12 @@ class TestProtocolClient implements ProtocolClient { throw new Error("Method not implemented."); } - subscribeResource(form: Form, next: (content: Content) => void, error?: ((error: Error) => void) | undefined, complete?: (() => void) | undefined): Promise { + subscribeResource( + form: Form, + next: (content: Content) => void, + error?: ((error: Error) => void) | undefined, + complete?: (() => void) | undefined + ): Promise { throw new Error("Method not implemented."); } @@ -95,7 +101,6 @@ class TestProtocolClient implements ProtocolClient { setSecurity(metadata: SecurityScheme[], credentials?: unknown): boolean { return true; } - } class TestProtocolClientFactory implements ProtocolClientFactory { @@ -116,16 +121,15 @@ class TestProtocolClientFactory implements ProtocolClientFactory { describe("Discovery Tests", () => { it("should be possible to use the exploreDirectory method", async () => { - - const servient = new Servient(); + const servient = new Servient(); servient.addClientFactory(new TestProtocolClientFactory()); - const WoT = await servient.start(); + const WoT = await servient.start(); - const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl); + const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl); - for await (const thingDescription of discoveryProcess) { - expect(thingDescription.title === "Directory Test TD"); - } - }); + for await (const thingDescription of discoveryProcess) { + expect(thingDescription.title === "Directory Test TD"); + } + }); }); From 7eddb7fd3b41ad426aa12862885b81444cb7550e Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 15 Dec 2023 17:29:11 +0100 Subject: [PATCH 08/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 24b46c85c..8a3c6aa5e 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -32,7 +32,7 @@ import Servient from "./servient"; import * as TD from "@node-wot/td-tools"; import * as TDT from "wot-thing-description-types"; import { ContentSerdes } from "./content-serdes"; -import Ajv, { ValidateFunction, ErrorObject } from "ajv"; +import { ValidateFunction, ErrorObject } from "ajv"; import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json"; import { DataSchemaValue, ExposedThingInit } from "wot-typescript-definitions"; import { SomeJSONSchema } from "ajv/dist/types/json-schema"; From 764d9fac271486127962a5eba30424b56969de24 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Fri, 15 Dec 2023 17:31:43 +0100 Subject: [PATCH 09/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/validation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index 6a31cbc4e..81deb5d91 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -14,7 +14,7 @@ ********************************************************************************/ import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json"; -import Ajv, { ValidateFunction } from "ajv"; +import Ajv from "ajv"; export function createWotAjvInstance() { return ( @@ -33,7 +33,7 @@ export function createWotAjvInstance() { } const ajv = createWotAjvInstance(); -const validateTd = ajv.compile(TDSchema) as ValidateFunction; +const validateTd = ajv.compile(TDSchema); export function isThingDescription(input: unknown): input is WoT.ThingDescription { return validateTd(input); From 05145677de4ff95aa625411804ca0adb66345389 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 09:19:41 +0100 Subject: [PATCH 10/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/wot-impl.ts | 59 ++++++++++++----------------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index fb2e0ccda..20603aa4f 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -22,60 +22,43 @@ import Helpers from "./helpers"; import { createLoggers } from "./logger"; import ContentManager from "./content-serdes"; import { ErrorObject } from "ajv"; -import { ThingDescription } from "wot-thing-description-types"; import { isThingDescription } from "./validation"; const { debug } = createLoggers("core", "wot-impl"); -// @ts-expect-error Typescript currently encounters an error here that *should* be a false positve -class ExploreDirectoryDatasource implements UnderlyingDefaultSource { - constructor(directoryOutput: WoT.DataSchemaValue) { - this.directoryOutput = directoryOutput; - } - - directoryOutput: WoT.DataSchemaValue; - - start(controller: ReadableStreamDefaultController) { - if (!(this.directoryOutput instanceof Array)) { - controller.error(new Error("Encountered an invalid output value.")); - controller.close(); - return; - } - - for (const outputValue of this.directoryOutput) { - if (!isThingDescription(outputValue)) { - const validationError = new Error("Validation of Thing Description failed"); - controller.error(validationError); - continue; - } - - controller.enqueue(outputValue); - } - - controller.close(); - } -} - class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess { - constructor(thingDescriptionStream: ReadableStream, filter?: WoT.ThingFilter) { + constructor(rawThingDescriptions: WoT.DataSchemaValue, filter?: WoT.ThingFilter) { this.filter = filter; this.done = false; - this.thingDescriptionStream = thingDescriptionStream; + this.rawThingDescriptions = rawThingDescriptions; } - thingDescriptionStream: ReadableStream; + rawThingDescriptions: WoT.DataSchemaValue; filter?: WoT.ThingFilter | undefined; done: boolean; error?: Error | undefined; async stop(): Promise { - await this.thingDescriptionStream.cancel(); this.done = true; } async *[Symbol.asyncIterator](): AsyncIterator { - // @ts-expect-error Typescript currently encounters an error here that *should* be a false positve - yield* this.thingDescriptionStream; + + 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 (!isThingDescription(outputValue)) { + this.error = new Error("Validation of Thing Description failed"); + continue; + } + + yield outputValue; + } + this.done = true; } } @@ -98,10 +81,8 @@ export default class WoTImpl { const thingsPropertyOutput = await consumedDirectoy.readProperty("things"); const rawThingDescriptions = await thingsPropertyOutput.value(); - const thingDescriptionDataSource = new ExploreDirectoryDatasource(rawThingDescriptions); - const thingDescriptionStream = new ReadableStream(thingDescriptionDataSource); - return new ThingDiscoveryProcess(thingDescriptionStream, filter); + return new ThingDiscoveryProcess(rawThingDescriptions, filter); } /** @inheritDoc */ From 57affb8b6521939c38b2a706d1fbcd2bd68d0600 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 09:34:24 +0100 Subject: [PATCH 11/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/wot-impl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index 20603aa4f..ae383d467 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -43,7 +43,6 @@ class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess { } async *[Symbol.asyncIterator](): AsyncIterator { - if (!(this.rawThingDescriptions instanceof Array)) { this.error = new Error("Encountered an invalid output value."); this.done = true; From 951ca10a5d378d433a03f20b6e013d2523840187 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:08:27 +0100 Subject: [PATCH 12/21] fixup! test: add test for `exploreDirectory` method --- packages/core/test/DiscoveryTest.ts | 112 +++++++++++++++++++--------- 1 file changed, 78 insertions(+), 34 deletions(-) diff --git a/packages/core/test/DiscoveryTest.ts b/packages/core/test/DiscoveryTest.ts index cb3d1ad4f..e2160f554 100644 --- a/packages/core/test/DiscoveryTest.ts +++ b/packages/core/test/DiscoveryTest.ts @@ -22,37 +22,57 @@ import Servient from "../src/servient"; import { Readable } from "stream"; import { expect } from "chai"; -const { debug } = createLoggers("core", "DiscoveryTest"); - -const directoryTdUrl = "test://localhost/.well-known/wot"; -const directoryThingsUrl = "test://localhost/things"; - -const directoryThingDescription = { - "@context": "https://www.w3.org/2022/wot/td/v1.1", - title: "Directory Test TD", - security: "nosec_sc", - securityDefinitions: { - nosec_sc: { - scheme: "nosec", +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: directoryThingsUrl, - }, - ], + 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/.well-known/wot"; +const directoryTdUrl2 = "test://[::1]/.well-known/wot"; + +const directoryTdTitle1 = "Directory Test TD 1"; +const directoryTdTitle2 = "Directory Test TD 2"; + +const directoryThingsUrl1 = "test://localhost/things1"; +const directoryThingsUrl2 = "test://localhost/things2"; + +const directoryThingDescription1 = createDirectoryTestTd(directoryTdTitle1, directoryThingsUrl1); +const directoryThingDescription2 = createDirectoryTestTd(directoryTdTitle2, directoryThingsUrl2); class TestProtocolClient implements ProtocolClient { async readResource(form: Form): Promise { - if (form.href === directoryThingsUrl) { - const buffer = Buffer.from(JSON.stringify([directoryThingDescription])); - const content = new Content("application/ld+json", Readable.from(buffer)); - return 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"); } throw new Error("Invalid URL"); @@ -80,11 +100,13 @@ class TestProtocolClient implements ProtocolClient { } async requestThingDescription(uri: string): Promise { - if (uri === directoryTdUrl) { - debug(`Found corrent URL ${directoryTdUrl} to fetch directory TD`); - const buffer = Buffer.from(JSON.stringify(directoryThingDescription)); - const content = new Content("application/td+json", Readable.from(buffer)); - return 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"); } throw Error("Invalid URL"); @@ -119,17 +141,39 @@ class TestProtocolClientFactory implements ProtocolClientFactory { } } -describe("Discovery Tests", () => { +describe.only("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); + console.log(discoveryProcess.error); + expect(discoveryProcess.error).to.eq(undefined); + }); + 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(directoryTdUrl); + const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl2); + let tdCounter = 0; for await (const thingDescription of discoveryProcess) { - expect(thingDescription.title === "Directory Test TD"); + error(`Encountered unexpected TD with title ${thingDescription.title}`); + tdCounter++; } + expect(tdCounter).to.eql(0); + expect(discoveryProcess.error).to.not.eq(undefined); }); }); From cddd356db58cadd1a5e5f619e1d605e5c0339db9 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:12:43 +0100 Subject: [PATCH 13/21] fixup! test: add test for `exploreDirectory` method --- packages/core/test/DiscoveryTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/DiscoveryTest.ts b/packages/core/test/DiscoveryTest.ts index e2160f554..22e775007 100644 --- a/packages/core/test/DiscoveryTest.ts +++ b/packages/core/test/DiscoveryTest.ts @@ -141,7 +141,7 @@ class TestProtocolClientFactory implements ProtocolClientFactory { } } -describe.only("Discovery Tests", () => { +describe("Discovery Tests", () => { it("should be possible to use the exploreDirectory method", async () => { const servient = new Servient(); servient.addClientFactory(new TestProtocolClientFactory()); From 99e102b39fadf90b7c22c029ed97dfee087afeb2 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:12:52 +0100 Subject: [PATCH 14/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/helpers.ts | 15 ++++++++++++--- packages/core/src/validation.ts | 24 ++---------------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 8a3c6aa5e..8b8f34e86 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -32,7 +32,7 @@ import Servient from "./servient"; import * as TD from "@node-wot/td-tools"; import * as TDT from "wot-thing-description-types"; import { ContentSerdes } from "./content-serdes"; -import { ValidateFunction, ErrorObject } from "ajv"; +import Ajv, { ValidateFunction, ErrorObject } from "ajv"; import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json"; import { DataSchemaValue, ExposedThingInit } from "wot-typescript-definitions"; import { SomeJSONSchema } from "ajv/dist/types/json-schema"; @@ -40,12 +40,21 @@ import { ThingInteraction, ThingModelHelpers } from "@node-wot/td-tools"; import { Resolver } from "@node-wot/td-tools/src/resolver-interface"; import { PropertyElement, DataSchema } from "wot-thing-description-types"; import { createLoggers } from "./logger"; -import { createWotAjvInstance } from "./validation"; const { debug, error, warn } = createLoggers("core", "helpers"); const tdSchema = TDSchema; -const ajv = createWotAjvInstance(); +// RegExps take from https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts +const ajv = new Ajv({ strict: false }) + .addFormat( + "iri-reference", + /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i + ) + .addFormat("uri", /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/) + .addFormat( + "date-time", + /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/ + ); export default class Helpers implements Resolver { static tsSchemaValidator = ajv.compile(Helpers.createExposeThingInitSchema(tdSchema)) as ValidateFunction; diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index 81deb5d91..5e8a6b353 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -13,28 +13,8 @@ * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 ********************************************************************************/ -import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json"; -import Ajv from "ajv"; - -export function createWotAjvInstance() { - return ( - new Ajv({ strict: false }) - // RegExps taken from https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts - .addFormat( - "iri-reference", - /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i - ) - .addFormat("uri", /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/) - .addFormat( - "date-time", - /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/ - ) - ); -} - -const ajv = createWotAjvInstance(); -const validateTd = ajv.compile(TDSchema); +import Helpers from "./helpers"; export function isThingDescription(input: unknown): input is WoT.ThingDescription { - return validateTd(input); + return Helpers.tsSchemaValidator(input); } From 6f11544e25aec76add2b7b978cb9c5cce77efe54 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:20:25 +0100 Subject: [PATCH 15/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/validation.ts | 6 ++++++ packages/core/src/wot-impl.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index 5e8a6b353..7a14bc048 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -13,8 +13,14 @@ * 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); +} diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index ae383d467..45ae28815 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -22,7 +22,7 @@ import Helpers from "./helpers"; import { createLoggers } from "./logger"; import ContentManager from "./content-serdes"; import { ErrorObject } from "ajv"; -import { isThingDescription } from "./validation"; +import { getLastValidationErrors, isThingDescription } from "./validation"; const { debug } = createLoggers("core", "wot-impl"); @@ -51,7 +51,7 @@ class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess { for (const outputValue of this.rawThingDescriptions) { if (!isThingDescription(outputValue)) { - this.error = new Error("Validation of Thing Description failed"); + this.error = getLastValidationErrors(); continue; } From 568ebccfe4205653c09da7846c0c9e2b977fbb90 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:21:50 +0100 Subject: [PATCH 16/21] refactor(core): use validation functions for requestThingDescription --- packages/core/src/wot-impl.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index 45ae28815..7688a6a45 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -21,7 +21,6 @@ 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"); @@ -91,14 +90,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 */ From 5e17c5f386a6d782674c4a68bee1e41bb182ee18 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:38:41 +0100 Subject: [PATCH 17/21] fixup! test: add test for `exploreDirectory` method --- packages/core/test/DiscoveryTest.ts | 32 ++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/core/test/DiscoveryTest.ts b/packages/core/test/DiscoveryTest.ts index 22e775007..9d1612e98 100644 --- a/packages/core/test/DiscoveryTest.ts +++ b/packages/core/test/DiscoveryTest.ts @@ -52,17 +52,21 @@ function createDiscoveryContent(td: unknown, contentType: string) { return content; } -const directoryTdUrl1 = "test://localhost/.well-known/wot"; -const directoryTdUrl2 = "test://[::1]/.well-known/wot"; +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, directoryThingsUrl2); class TestProtocolClient implements ProtocolClient { async readResource(form: Form): Promise { @@ -73,6 +77,8 @@ class TestProtocolClient implements ProtocolClient { 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"); @@ -107,6 +113,9 @@ class TestProtocolClient implements ProtocolClient { 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"); @@ -160,7 +169,7 @@ describe("Discovery Tests", () => { expect(discoveryProcess.error).to.eq(undefined); }); - it("should be possible to use the exploreDirectory method", async () => { + 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()); @@ -176,4 +185,21 @@ describe("Discovery Tests", () => { 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); + }); }); From a06e024337b672289a47453a6e3d5caa92b0ce12 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:43:36 +0100 Subject: [PATCH 18/21] fixup! test: add test for `exploreDirectory` method --- packages/core/test/DiscoveryTest.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/core/test/DiscoveryTest.ts b/packages/core/test/DiscoveryTest.ts index 9d1612e98..8d827bc96 100644 --- a/packages/core/test/DiscoveryTest.ts +++ b/packages/core/test/DiscoveryTest.ts @@ -202,4 +202,24 @@ describe("Discovery Tests", () => { 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); + }); }); From 66104cf396693b3b363957c96ae5142b76b49b30 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:43:46 +0100 Subject: [PATCH 19/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/wot-impl.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index 7688a6a45..00754fdb8 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -49,6 +49,10 @@ class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess { } for (const outputValue of this.rawThingDescriptions) { + if (this.done) { + return; + } + if (!isThingDescription(outputValue)) { this.error = getLastValidationErrors(); continue; From c8fc93f735c56da021c87bf724e54addc3a5ce76 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 11:52:51 +0100 Subject: [PATCH 20/21] fixup! test: add test for `exploreDirectory` method --- packages/core/test/DiscoveryTest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/test/DiscoveryTest.ts b/packages/core/test/DiscoveryTest.ts index 8d827bc96..3b41b0f5e 100644 --- a/packages/core/test/DiscoveryTest.ts +++ b/packages/core/test/DiscoveryTest.ts @@ -66,7 +66,7 @@ const directoryThingsUrl3 = "test://localhost/things3"; const directoryThingDescription1 = createDirectoryTestTd(directoryTdTitle1, directoryThingsUrl1); const directoryThingDescription2 = createDirectoryTestTd(directoryTdTitle2, directoryThingsUrl2); -const directoryThingDescription3 = createDirectoryTestTd(directoryTdTitle3, directoryThingsUrl2); +const directoryThingDescription3 = createDirectoryTestTd(directoryTdTitle3, directoryThingsUrl3); class TestProtocolClient implements ProtocolClient { async readResource(form: Form): Promise { @@ -165,7 +165,6 @@ describe("Discovery Tests", () => { tdCounter++; } expect(tdCounter).to.eql(1); - console.log(discoveryProcess.error); expect(discoveryProcess.error).to.eq(undefined); }); From 7cc777fd58fd534f2bae031a24a9bf9fbf1cc083 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 21 Dec 2023 12:57:31 +0100 Subject: [PATCH 21/21] fixup! feat(core): implement `exploreDirectory` method --- packages/core/src/helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 8b8f34e86..1dfd7e84b 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -55,7 +55,6 @@ const ajv = new Ajv({ strict: false }) "date-time", /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/ ); - export default class Helpers implements Resolver { static tsSchemaValidator = ajv.compile(Helpers.createExposeThingInitSchema(tdSchema)) as ValidateFunction;