Skip to content

Commit

Permalink
feat: add initial requestThingDescription implementation (#1166)
Browse files Browse the repository at this point in the history
* feat: add initial requestThingDescription implementation
  • Loading branch information
JKRhb authored Nov 29, 2023
1 parent 143c8a0 commit 0a6463b
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 29 deletions.
18 changes: 18 additions & 0 deletions packages/binding-coap/src/coap-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,24 @@ export default class CoapClient implements ProtocolClient {
});
}

/**
* @inheritdoc
*/
requestThingDescription(uri: string): Promise<Content> {
const options: CoapRequestParams = this.uriToOptions(uri);
const req = this.agent.request(options);

req.setOption("Accept", "application/td+json");
return new Promise<Content>((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<void> {
// do nothing
}
Expand Down
17 changes: 17 additions & 0 deletions packages/binding-coap/src/coaps-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,23 @@ export default class CoapsClient implements ProtocolClient {
});
}

/**
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
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<void> {
// do nothing
}
Expand Down
71 changes: 43 additions & 28 deletions packages/binding-file/src/file-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Content> {
const resource = fs.createReadStream(filepath);
const resourceContentType = contentType ?? mapFileExtensionToContentType(filepath);
return new Content(resourceContentType, resource);
}

public async readResource(form: Form): Promise<Content> {
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<void> {
Expand All @@ -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<Content> {
return this.readFile(uri, "application/td+json");
}

public async subscribeResource(
form: Form,
next: (value: Content) => void,
Expand Down
12 changes: 12 additions & 0 deletions packages/binding-http/src/http-client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ export default class HttpClient implements ProtocolClient {
}
}

/**
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
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<void> {
// do nothing
}
Expand Down
7 changes: 7 additions & 0 deletions packages/binding-mbus/src/mbus-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ export default class MBusClient implements ProtocolClient {
throw new Error("Method not implemented.");
}

/**
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented");
}

async start(): Promise<void> {
// do nothing
}
Expand Down
7 changes: 7 additions & 0 deletions packages/binding-modbus/src/modbus-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ export default class ModbusClient implements ProtocolClient {
});
}

/**
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented");
}

async start(): Promise<void> {
// do nothing
}
Expand Down
7 changes: 7 additions & 0 deletions packages/binding-mqtt/src/mqtt-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ export default class MqttClient implements ProtocolClient {
}
}

/**
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented");
}

public async start(): Promise<void> {
// do nothing
}
Expand Down
7 changes: 7 additions & 0 deletions packages/binding-netconf/src/netconf-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ export default class NetconfClient implements ProtocolClient {
throw unimplementedError;
}

/**
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented");
}

public async start(): Promise<void> {
// do nothing
}
Expand Down
7 changes: 7 additions & 0 deletions packages/binding-opcua/src/opcua-protocol-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,13 @@ export class OPCUAProtocolClient implements ProtocolClient {
});
}

/**
* @inheritdoc
*/
public async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented");
}

start(): Promise<void> {
debug("start: Sorry not implemented");
throw new Error("Method not implemented.");
Expand Down
7 changes: 7 additions & 0 deletions packages/binding-websockets/src/ws-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Content> {
throw new Error("Method not implemented");
}

public async start(): Promise<void> {
// do nothing
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/protocol-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ export interface ProtocolClient {
complete?: () => void
): Promise<Subscription>;

/**
* 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<Content>;

/** start the client (ensure it is ready to send requests) */
start(): Promise<void>;
/** stop the client */
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/wot-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -39,8 +41,21 @@ export default class WoTImpl {
throw new Error("not implemented");
}

/** @inheritDoc */
async requestThingDescription(url: string): Promise<WoT.ThingDescription> {
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 */
Expand Down
12 changes: 12 additions & 0 deletions packages/core/test/ClientTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ class TDClient implements ProtocolClient {
return new Subscription();
}

async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented.");
}

public async start(): Promise<void> {
// do nothing
}
Expand Down Expand Up @@ -230,6 +234,10 @@ class TrapClient implements ProtocolClient {
return new Subscription();
}

async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented.");
}

public async start(): Promise<void> {
// do nothing
}
Expand Down Expand Up @@ -293,6 +301,10 @@ class TestProtocolClient implements ProtocolClient {
throw new Error("Method not implemented.");
}

async requestThingDescription(uri: string): Promise<Content> {
throw new Error("Method not implemented.");
}

async start(): Promise<void> {
throw new Error("Method not implemented.");
}
Expand Down

0 comments on commit 0a6463b

Please sign in to comment.