From cd96956f6b3054fd329b49d58f37eb40670b7108 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 21 Nov 2023 15:36:33 +0100 Subject: [PATCH 1/7] feat: add initial requestThingDescription implementation --- packages/binding-coap/src/coap-client.ts | 18 ++++++++++++++++++ packages/binding-coap/src/coaps-client.ts | 17 +++++++++++++++++ packages/binding-file/src/file-client.ts | 7 +++++++ packages/binding-http/src/http-client-impl.ts | 12 ++++++++++++ packages/binding-mbus/src/mbus-client.ts | 7 +++++++ packages/binding-modbus/src/modbus-client.ts | 7 +++++++ packages/binding-mqtt/src/mqtt-client.ts | 7 +++++++ packages/binding-netconf/src/netconf-client.ts | 7 +++++++ .../binding-opcua/src/opcua-protocol-client.ts | 7 +++++++ packages/binding-websockets/src/ws-client.ts | 7 +++++++ packages/core/src/protocol-interfaces.ts | 10 ++++++++++ packages/core/src/wot-impl.ts | 15 ++++++++++++++- packages/core/test/ClientTest.ts | 12 ++++++++++++ 13 files changed, 132 insertions(+), 1 deletion(-) diff --git a/packages/binding-coap/src/coap-client.ts b/packages/binding-coap/src/coap-client.ts index 4d0740ace..bf4414ff6 100644 --- a/packages/binding-coap/src/coap-client.ts +++ b/packages/binding-coap/src/coap-client.ts @@ -183,6 +183,24 @@ export default class CoapClient implements ProtocolClient { }); } + /** + * @inheritdoc + */ + requestThingDescription(uri: string): Promise { + const options: CoapRequestParams = this.uriToOptions(uri); + const req = this.agent.request(options); + + req.setOption("Accept", "application/td+json"); + return new Promise((resolve, reject) => { + req.on("response", (res: IncomingMessage) => { + const contentType = (res.headers["Content-Format"] as string) ?? "application/td+json"; + resolve(new Content(contentType, Readable.from(res.payload))); + }); + req.on("error", (err: Error) => reject(err)); + req.end(); + }); + } + public async start(): Promise { // do nothing } diff --git a/packages/binding-coap/src/coaps-client.ts b/packages/binding-coap/src/coaps-client.ts index 2113c4f4f..31105a6dd 100644 --- a/packages/binding-coap/src/coaps-client.ts +++ b/packages/binding-coap/src/coaps-client.ts @@ -139,6 +139,23 @@ export default class CoapsClient implements ProtocolClient { }); } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + const response = await coaps.request(uri, "get", undefined, { + // FIXME: Add accept option + // Currently not supported by node-coap-client + }); + + // TODO: Respect Content-Format in response. + // Currently not really well supported by node-coap-client + const contentType = "application/td+json"; + const payload = response.payload ?? Buffer.alloc(0); + + return new Content(contentType, Readable.from(payload)); + } + public async start(): Promise { // do nothing } diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index 31c3d005a..968c62749 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -72,6 +72,13 @@ export default class FileClient implements ProtocolClient { throw new Error("FileClient does not implement unlink"); } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + throw new Error("Not implemented"); + } + public async subscribeResource( form: Form, next: (value: Content) => void, diff --git a/packages/binding-http/src/http-client-impl.ts b/packages/binding-http/src/http-client-impl.ts index de5c93182..dc09688f1 100644 --- a/packages/binding-http/src/http-client-impl.ts +++ b/packages/binding-http/src/http-client-impl.ts @@ -212,6 +212,18 @@ export default class HttpClient implements ProtocolClient { } } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + const headers: HeadersInit = { + Accept: "application/td+json", + }; + const response = await fetch(uri, { headers }); + const body = ProtocolHelpers.toNodeStream(response.body as Readable); + return new Content(response.headers.get("content-type") ?? "application/td+json", body); + } + public async start(): Promise { // do nothing } diff --git a/packages/binding-mbus/src/mbus-client.ts b/packages/binding-mbus/src/mbus-client.ts index 63a8c24cb..783b6972d 100644 --- a/packages/binding-mbus/src/mbus-client.ts +++ b/packages/binding-mbus/src/mbus-client.ts @@ -61,6 +61,13 @@ export default class MBusClient implements ProtocolClient { throw new Error("Method not implemented."); } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented"); + } + async start(): Promise { // do nothing } diff --git a/packages/binding-modbus/src/modbus-client.ts b/packages/binding-modbus/src/modbus-client.ts index f375d227d..2a9b3c4e4 100644 --- a/packages/binding-modbus/src/modbus-client.ts +++ b/packages/binding-modbus/src/modbus-client.ts @@ -131,6 +131,13 @@ export default class ModbusClient implements ProtocolClient { }); } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented"); + } + async start(): Promise { // do nothing } diff --git a/packages/binding-mqtt/src/mqtt-client.ts b/packages/binding-mqtt/src/mqtt-client.ts index ef3b5d1c9..079e3e287 100644 --- a/packages/binding-mqtt/src/mqtt-client.ts +++ b/packages/binding-mqtt/src/mqtt-client.ts @@ -161,6 +161,13 @@ export default class MqttClient implements ProtocolClient { } } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented"); + } + public async start(): Promise { // do nothing } diff --git a/packages/binding-netconf/src/netconf-client.ts b/packages/binding-netconf/src/netconf-client.ts index 90d8892cb..1b2762efa 100644 --- a/packages/binding-netconf/src/netconf-client.ts +++ b/packages/binding-netconf/src/netconf-client.ts @@ -155,6 +155,13 @@ export default class NetconfClient implements ProtocolClient { throw unimplementedError; } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented"); + } + public async start(): Promise { // do nothing } diff --git a/packages/binding-opcua/src/opcua-protocol-client.ts b/packages/binding-opcua/src/opcua-protocol-client.ts index 524564c1c..f3056e33c 100644 --- a/packages/binding-opcua/src/opcua-protocol-client.ts +++ b/packages/binding-opcua/src/opcua-protocol-client.ts @@ -430,6 +430,13 @@ export class OPCUAProtocolClient implements ProtocolClient { }); } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented"); + } + start(): Promise { debug("start: Sorry not implemented"); throw new Error("Method not implemented."); diff --git a/packages/binding-websockets/src/ws-client.ts b/packages/binding-websockets/src/ws-client.ts index cdf79dd12..def44c025 100644 --- a/packages/binding-websockets/src/ws-client.ts +++ b/packages/binding-websockets/src/ws-client.ts @@ -66,6 +66,13 @@ export default class WebSocketClient implements ProtocolClient { throw new Error("Websocket client does not implement subscribeResource"); } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented"); + } + public async start(): Promise { // do nothing } diff --git a/packages/core/src/protocol-interfaces.ts b/packages/core/src/protocol-interfaces.ts index 68ded1233..d36357a81 100644 --- a/packages/core/src/protocol-interfaces.ts +++ b/packages/core/src/protocol-interfaces.ts @@ -66,6 +66,16 @@ export interface ProtocolClient { complete?: () => void ): Promise; + /** + * Requests a single Thing Description from a given {@link uri}. + * + * The result is returned asynchronously as {@link Content}, which has to + * be deserialized and validated by the upper layers of the implementation. + * + * @param uri + */ + requestThingDescription(uri: string): Promise; + /** start the client (ensure it is ready to send requests) */ start(): Promise; /** stop the client */ diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index 55ac7265b..420343952 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -20,6 +20,7 @@ import ExposedThing from "./exposed-thing"; import ConsumedThing from "./consumed-thing"; import Helpers from "./helpers"; import { createLoggers } from "./logger"; +import ContentManager from "./content-serdes"; const { debug } = createLoggers("core", "wot-impl"); @@ -39,8 +40,20 @@ export default class WoTImpl { throw new Error("not implemented"); } + /** @inheritDoc */ async requestThingDescription(url: string): Promise { - throw new Error("not implemented"); + const uriScheme = new URL(url).protocol.split(":")[0]; + const client = this.srv.getClientFor(uriScheme); + const result = await client.requestThingDescription(url); + + const value = ContentManager.contentToValue({ type: result.type, body: await result.toBuffer() }, {}); + + if (value instanceof Object) { + // TODO: Add validation step + return value as WoT.ThingDescription; + } + + throw new Error("Not found."); } /** @inheritDoc */ diff --git a/packages/core/test/ClientTest.ts b/packages/core/test/ClientTest.ts index 56a28678a..41c97edaa 100644 --- a/packages/core/test/ClientTest.ts +++ b/packages/core/test/ClientTest.ts @@ -156,6 +156,10 @@ class TDClient implements ProtocolClient { return new Subscription(); } + async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented."); + } + public async start(): Promise { // do nothing } @@ -230,6 +234,10 @@ class TrapClient implements ProtocolClient { return new Subscription(); } + async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented."); + } + public async start(): Promise { // do nothing } @@ -293,6 +301,10 @@ class TestProtocolClient implements ProtocolClient { throw new Error("Method not implemented."); } + async requestThingDescription(uri: string): Promise { + throw new Error("Method not implemented."); + } + async start(): Promise { throw new Error("Method not implemented."); } From 3a7c741e48a155b01039b3745db65fedd402755f Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 23 Nov 2023 08:27:13 +0100 Subject: [PATCH 2/7] feat: implement requestThingDescription for FileClient --- packages/binding-file/src/file-client.ts | 66 +++++++++++++----------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index 968c62749..b9235a54d 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -24,40 +24,48 @@ import path = require("path"); const { debug, warn } = createLoggers("binding-file", "file-client"); +/** + * Used to determine the Content-Type of a file from the extension in its + * {@link filePath} if no explicit Content-Type is defined. + * + * @param filepath The file paththe Content-Type is determined for. + * @returns An appropriate Content-Type or `application/octet-stream` as a fallback. + */ +function mapFileExtensionToContentType(filepath: string) { + const fileExtension = path.extname(filepath); + debug(`FileClient found '${fileExtension}' extension`); + switch (fileExtension) { + case ".txt": + case ".log": + case ".ini": + case ".cfg": + return "text/plain"; + case ".json": + return "application/json"; + case ".jsontd": + return "application/td+json"; + case ".jsonld": + return "application/ld+json"; + default: + warn(`FileClient cannot determine media type for path '${filepath}'`); + return "application/octet-stream"; + } +} + export default class FileClient implements ProtocolClient { public toString(): string { return "[FileClient]"; } + private async readFile(filepath: string, contentType?: string): Promise { + const resource = fs.createReadStream(filepath); + const resourceContentType = contentType ?? mapFileExtensionToContentType(filepath); + return new Content(resourceContentType, resource); + } + public async readResource(form: Form): Promise { - const filepath = form.href.split("//"); - const resource = fs.createReadStream(filepath[1]); - const extension = path.extname(filepath[1]); - debug(`FileClient found '${extension}' extension`); - let contentType; - if (form.contentType != null) { - contentType = form.contentType; - } else { - // *guess* contentType based on file extension - contentType = "application/octet-stream"; - switch (extension) { - case ".txt": - case ".log": - case ".ini": - case ".cfg": - contentType = "text/plain"; - break; - case ".json": - contentType = "application/json"; - break; - case ".jsonld": - contentType = "application/ld+json"; - break; - default: - warn(`FileClient cannot determine media type of '${form.href}'`); - } - } - return new Content(contentType, resource); + const filepath = form.href.split("//")[1]; + return this.readFile(filepath, form.contentType); } public async writeResource(form: Form, content: Content): Promise { @@ -76,7 +84,7 @@ export default class FileClient implements ProtocolClient { * @inheritdoc */ public async requestThingDescription(uri: string): Promise { - throw new Error("Not implemented"); + return this.readFile(uri, "application/td+json"); } public async subscribeResource( From 243f607415a363f5943bc83e552e25555fa9fb2d Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 23 Nov 2023 08:50:01 +0100 Subject: [PATCH 3/7] fixup! feat: add initial requestThingDescription implementation --- packages/core/src/wot-impl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index 420343952..da07dfb89 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -21,6 +21,7 @@ import ConsumedThing from "./consumed-thing"; import Helpers from "./helpers"; import { createLoggers } from "./logger"; import ContentManager from "./content-serdes"; +import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json"; const { debug } = createLoggers("core", "wot-impl"); @@ -44,12 +45,11 @@ export default class WoTImpl { async requestThingDescription(url: string): Promise { const uriScheme = new URL(url).protocol.split(":")[0]; const client = this.srv.getClientFor(uriScheme); - const result = await client.requestThingDescription(url); + const content = await client.requestThingDescription(url); - const value = ContentManager.contentToValue({ type: result.type, body: await result.toBuffer() }, {}); + const value = ContentManager.contentToValue({ type: content.type, body: await content.toBuffer() }, TDSchema); if (value instanceof Object) { - // TODO: Add validation step return value as WoT.ThingDescription; } From 907b429a5e4400de62a5999e59a24c18850e62a4 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 23 Nov 2023 10:40:59 +0100 Subject: [PATCH 4/7] fixup! feat: implement requestThingDescription for FileClient --- packages/binding-file/src/file-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index b9235a54d..68191e9db 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -28,7 +28,7 @@ const { debug, warn } = createLoggers("binding-file", "file-client"); * Used to determine the Content-Type of a file from the extension in its * {@link filePath} if no explicit Content-Type is defined. * - * @param filepath The file paththe Content-Type is determined for. + * @param filepath The file path the Content-Type is determined for. * @returns An appropriate Content-Type or `application/octet-stream` as a fallback. */ function mapFileExtensionToContentType(filepath: string) { From b67162da2984f44c487054258d9c74736b5779e8 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 23 Nov 2023 10:51:09 +0100 Subject: [PATCH 5/7] fixup! feat: add initial requestThingDescription implementation --- packages/core/src/wot-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index da07dfb89..e41ddf0f7 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -43,7 +43,7 @@ export default class WoTImpl { /** @inheritDoc */ async requestThingDescription(url: string): Promise { - const uriScheme = new URL(url).protocol.split(":")[0]; + const uriScheme = Helpers.extractScheme(url); const client = this.srv.getClientFor(uriScheme); const content = await client.requestThingDescription(url); From 23af943a1b54df13309b512de5ee69a1f56b9bb1 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 23 Nov 2023 11:27:34 +0100 Subject: [PATCH 6/7] fixup! feat: implement requestThingDescription for FileClient --- packages/binding-file/src/file-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/binding-file/src/file-client.ts b/packages/binding-file/src/file-client.ts index 68191e9db..0ff48efd0 100644 --- a/packages/binding-file/src/file-client.ts +++ b/packages/binding-file/src/file-client.ts @@ -64,7 +64,7 @@ export default class FileClient implements ProtocolClient { } public async readResource(form: Form): Promise { - const filepath = form.href.split("//")[1]; + const filepath = new URL(form.href).pathname; return this.readFile(filepath, form.contentType); } From 5aeb45dde8507cb205e5f5c406e0b1f7528a15f1 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 23 Nov 2023 12:28:16 +0100 Subject: [PATCH 7/7] fixup! feat: add initial requestThingDescription implementation --- packages/core/src/wot-impl.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/core/src/wot-impl.ts b/packages/core/src/wot-impl.ts index e41ddf0f7..6a5ed96c4 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -21,7 +21,7 @@ import ConsumedThing from "./consumed-thing"; import Helpers from "./helpers"; import { createLoggers } from "./logger"; import ContentManager from "./content-serdes"; -import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json"; +import { ErrorObject } from "ajv"; const { debug } = createLoggers("core", "wot-impl"); @@ -46,14 +46,16 @@ export default class WoTImpl { const uriScheme = Helpers.extractScheme(url); const client = this.srv.getClientFor(uriScheme); const content = await client.requestThingDescription(url); + const value = ContentManager.contentToValue({ type: content.type, body: await content.toBuffer() }, {}); - const value = ContentManager.contentToValue({ type: content.type, body: await content.toBuffer() }, TDSchema); + const isValidThingDescription = Helpers.tsSchemaValidator(value); - if (value instanceof Object) { - return value as WoT.ThingDescription; + if (!isValidThingDescription) { + const errors = Helpers.tsSchemaValidator.errors?.map((o: ErrorObject) => o.message).join("\n"); + throw new Error(errors); } - throw new Error("Not found."); + return value as WoT.ThingDescription; } /** @inheritDoc */