diff --git a/packages/binding-coap/src/coap-client.ts b/packages/binding-coap/src/coap-client.ts index f27599259..9cddc9033 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 a76168166..6379f6a9c 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 91d4a2305..f52c15c42 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 path the 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 = new URL(form.href).pathname; + return this.readFile(filepath, form.contentType); } public async writeResource(form: Form, content: Content): Promise { @@ -72,6 +80,13 @@ export default class FileClient implements ProtocolClient { throw new Error("FileClient does not implement unlink"); } + /** + * @inheritdoc + */ + public async requestThingDescription(uri: string): Promise { + return this.readFile(uri, "application/td+json"); + } + 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 24b1f9422..553d2af04 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 c412b399c..740940106 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 076be25db..400c2d531 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 828f6b77f..a87f7c64e 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 97a564b74..872e2e90d 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 2df14aa2a..85ac6813d 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 ade221fe9..3671ace3c 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 768ed0870..44e41b007 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 33f29a2e0..42ca61395 100644 --- a/packages/core/src/wot-impl.ts +++ b/packages/core/src/wot-impl.ts @@ -20,6 +20,8 @@ import ExposedThing from "./exposed-thing"; import ConsumedThing from "./consumed-thing"; import Helpers from "./helpers"; import { createLoggers } from "./logger"; +import ContentManager from "./content-serdes"; +import { ErrorObject } from "ajv"; const { debug } = createLoggers("core", "wot-impl"); @@ -39,8 +41,21 @@ export default class WoTImpl { throw new Error("not implemented"); } + /** @inheritDoc */ async requestThingDescription(url: string): Promise { - throw new Error("not implemented"); + 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 isValidThingDescription = Helpers.tsSchemaValidator(value); + + if (!isValidThingDescription) { + const errors = Helpers.tsSchemaValidator.errors?.map((o: ErrorObject) => o.message).join("\n"); + throw new Error(errors); + } + + return value as WoT.ThingDescription; } /** @inheritDoc */ diff --git a/packages/core/test/ClientTest.ts b/packages/core/test/ClientTest.ts index 8c3ded6f6..3c1a2053f 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."); }