Skip to content

Commit

Permalink
feat(coap-server): add initial meta operation support (#1086)
Browse files Browse the repository at this point in the history
* feat(coap-server): add support for property meta operations

* fixup! feat(coap-server): add support for property meta operations

* fixup! feat(coap-server): add support for property meta operations

* test(coap-server): add basic tests for property meta operations
  • Loading branch information
JKRhb authored Sep 22, 2023
1 parent a38b05b commit aae70f4
Show file tree
Hide file tree
Showing 2 changed files with 299 additions and 15 deletions.
145 changes: 130 additions & 15 deletions packages/binding-coap/src/coap-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { Readable } from "stream";
import { MdnsIntroducer } from "./mdns-introducer";
import { PropertyElement, DataSchema } from "wot-thing-description-types";
import { CoapServerConfig } from "./coap";
import { DataSchemaValue } from "wot-typescript-definitions";

const { debug, warn, info, error } = createLoggers("binding-coap", "coap-server");

Expand Down Expand Up @@ -171,6 +172,8 @@ export default class CoapServer implements ProtocolServer {
for (const offeredMediaType of offeredMediaTypes) {
const base = this.createThingBase(address, port, urlPath);

this.fillInMetaPropertiesBindingData(thing, base, offeredMediaType);

this.fillInPropertyBindingData(thing, base, offeredMediaType);
this.fillInActionBindingData(thing, base, offeredMediaType);
this.fillInEventBindingData(thing, base, offeredMediaType);
Expand All @@ -182,17 +185,60 @@ export default class CoapServer implements ProtocolServer {
return `${this.scheme}://${address}:${port}/${encodeURIComponent(urlPath)}`;
}

private fillInMetaPropertiesBindingData(thing: ExposedThing, base: string, offeredMediaType: string) {
const opValues = this.createPropertyMetaOpValues(thing);

if (opValues.length === 0) {
return;
}

if (thing.forms == null) {
thing.forms = [];
}

const form = this.createAffordanceForm(base, this.PROPERTY_DIR, offeredMediaType, opValues, thing.uriVariables);

thing.forms.push(form);
}

private getReadableProperties(thing: ExposedThing) {
return Object.entries(thing.properties).filter(([_, value]) => value.writeOnly !== true);
}

private createPropertyMetaOpValues(thing: ExposedThing): string[] {
const properties = Object.values(thing.properties);
const numberOfProperties = properties.length;

if (numberOfProperties === 0) {
return [];
}

const readableProperties = this.getReadableProperties(thing).length;

const opValues: string[] = [];

if (readableProperties > 0) {
opValues.push("readmultipleproperties");
}

if (readableProperties === numberOfProperties) {
opValues.push("readallproperties");
}

return opValues;
}

private fillInPropertyBindingData(thing: ExposedThing, base: string, offeredMediaType: string) {
for (const [propertyName, property] of Object.entries(thing.properties)) {
const opValues = ProtocolHelpers.getPropertyOpValues(property);
const form = this.createAffordanceForm(
base,
this.PROPERTY_DIR,
propertyName,
offeredMediaType,
opValues,
property.uriVariables,
thing.uriVariables
thing.uriVariables,
propertyName,
property.uriVariables
);

property.forms.push(form);
Expand All @@ -205,11 +251,11 @@ export default class CoapServer implements ProtocolServer {
const form = this.createAffordanceForm(
base,
this.ACTION_DIR,
actionName,
offeredMediaType,
"invokeaction",
action.uriVariables,
thing.uriVariables
thing.uriVariables,
actionName,
action.uriVariables
);

action.forms.push(form);
Expand All @@ -222,11 +268,11 @@ export default class CoapServer implements ProtocolServer {
const form = this.createAffordanceForm(
base,
this.EVENT_DIR,
eventName,
offeredMediaType,
["subscribeevent", "unsubscribeevent"],
event.uriVariables,
thing.uriVariables
thing.uriVariables,
eventName,
event.uriVariables
);

event.forms.push(form);
Expand All @@ -237,19 +283,23 @@ export default class CoapServer implements ProtocolServer {
private createAffordanceForm(
base: string,
affordancePathSegment: string,
affordanceName: string,
offeredMediaType: string,
opValues: string | string[],
affordanceUriVariables: PropertyElement["uriVariables"] = {},
thingUriVariables: PropertyElement["uriVariables"] = {}
thingUriVariables: PropertyElement["uriVariables"],
affordanceName?: string,
affordanceUriVariables?: PropertyElement["uriVariables"]
): TD.Form {
const affordanceNamePattern = Helpers.updateInteractionNameWithUriVariablePattern(
affordanceName,
affordanceName ?? "",
affordanceUriVariables,
thingUriVariables
);

const href = `${base}/${affordancePathSegment}/${encodeURIComponent(affordanceNamePattern)}`;
let href = `${base}/${affordancePathSegment}`;

if (affordanceNamePattern.length > 0) {
href += `/${encodeURIComponent(affordanceNamePattern)}`;
}

const form = new TD.Form(href, offeredMediaType);
form.op = opValues;
Expand Down Expand Up @@ -473,7 +523,7 @@ export default class CoapServer implements ProtocolServer {
const property = thing.properties[affordanceKey];

if (property == null) {
this.sendNotFoundResponse(res);
this.handlePropertiesRequest(req, contentType, thing, res);
return;
}

Expand All @@ -498,6 +548,71 @@ export default class CoapServer implements ProtocolServer {
}
}

private async handlePropertiesRequest(
req: IncomingMessage,
contentType: string,
thing: ExposedThing,
res: OutgoingMessage
) {
const forms = thing.forms;

if (forms == null) {
this.sendNotFoundResponse(res);
return;
}

switch (req.method) {
case "GET":
this.handleReadMultipleProperties(forms, req, contentType, thing, res);
break;
default:
this.sendMethodNotAllowedResponse(res);
break;
}
}

private async handleReadMultipleProperties(
forms: TD.Form[],
req: IncomingMessage,
contentType: string,
thing: ExposedThing,
res: OutgoingMessage
) {
try {
const interactionOptions = this.createInteractionOptions(
forms,
thing,
req,
contentType,
thing.uriVariables
);
const readablePropertyKeys = this.getReadableProperties(thing).map(([key, _]) => key);
const contentMap = await thing.handleReadMultipleProperties(readablePropertyKeys, interactionOptions);

const recordResponse: Record<string, DataSchemaValue> = {};
for (const [key, content] of contentMap.entries()) {
const value = ContentSerdes.get().contentToValue(
{ type: ContentSerdes.DEFAULT, body: await content.toBuffer() },
{}
);

if (value == null) {
// TODO: How should this case be handled?
continue;
}

recordResponse[key] = value;
}

const content = ContentSerdes.get().valueToContent(recordResponse, undefined, contentType);
this.streamContentResponse(res, content);
} catch (err) {
const errorMessage = `${err}`;
error(`CoapServer on port ${this.getPort()} got internal error on read '${req.url}': ${errorMessage}`);
this.sendResponse(res, "5.00", errorMessage);
}
}

private async handleReadProperty(
property: PropertyElement,
req: IncomingMessage,
Expand Down
169 changes: 169 additions & 0 deletions packages/binding-coap/test/coap-server-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,4 +471,173 @@ class CoapServerTest {
await coapClient.stop();
await coapServer.stop();
}

@test async "should report allproperties excluding non-JSON properties"() {
const port = 5683;
const coapServer = new CoapServer({ port });
const servient = new Servient();

await coapServer.start(servient);

const tdTemplate: WoT.ExposedThingInit = {
title: "TestA",
properties: {
image: {
forms: [
{
contentType: "image/svg+xml",
},
],
},
testInteger: {
type: "integer",
},
testBoolean: {
type: "boolean",
},
testString: {
type: "string",
},
testObject: {
type: "object",
},
testArray: {
type: "array",
},
},
};
const testThing = new ExposedThing(servient, tdTemplate);

const image = "<svg xmlns='http://www.w3.org/2000/svg'><text>FOO</text></svg>";
const integer = 123;
const boolean = true;
const string = "ABCD";
const object = { t1: "xyz", i: 77 };
const array = ["x", "y", "z"];
testThing.setPropertyReadHandler("image", async (_) => image);
testThing.setPropertyReadHandler("testInteger", async (_) => integer);
testThing.setPropertyReadHandler("testBoolean", async (_) => boolean);
testThing.setPropertyReadHandler("testString", async (_) => string);
testThing.setPropertyReadHandler("testObject", async (_) => object);
testThing.setPropertyReadHandler("testArray", async (_) => array);

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testThing.properties.image.forms = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testThing.properties.testInteger.forms = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testThing.properties.testBoolean.forms = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testThing.properties.testString.forms = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testThing.properties.testObject.forms = [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testThing.properties.testArray.forms = [];

await coapServer.expose(testThing, tdTemplate);

const coapClient = new CoapClient(coapServer);

const decodeContent = async (content: Content) => JSON.parse((await content.toBuffer()).toString());

const baseUri = `coap://localhost:${port}/testa/properties`;

// check values one by one first
const responseInteger = await coapClient.readResource(new TD.Form(`${baseUri}/testInteger`));
expect(await decodeContent(responseInteger)).to.equal(integer);
const responseBoolean = await coapClient.readResource(new TD.Form(`${baseUri}/testBoolean`));
expect(await decodeContent(responseBoolean)).to.equal(boolean);
const responseString = await coapClient.readResource(new TD.Form(`${baseUri}/testString`));
expect(await decodeContent(responseString)).to.equal(string);
const responseObject = await coapClient.readResource(new TD.Form(`${baseUri}/testObject`));
expect(await decodeContent(responseObject)).to.deep.equal(object);
const responseArray = await coapClient.readResource(new TD.Form(`${baseUri}/testArray`));
expect(await decodeContent(responseArray)).to.deep.equal(array);

// check values of readallproperties
const responseAll = await coapClient.readResource(new TD.Form(baseUri));
expect(await decodeContent(responseAll)).to.deep.equal({
image: image,
testInteger: integer,
testBoolean: boolean,
testString: string,
testObject: object,
testArray: array,
});

await coapServer.stop();
await coapClient.stop();
}

@test async "should reject requests for undefined meta operations"() {
const coapServer = new CoapServer();
const servient = new Servient();

await coapServer.start(servient);

const testThingWithoutForms = new ExposedThing(servient, {
title: "Test",
});

await coapServer.expose(testThingWithoutForms);

await new Promise<void>((resolve) => {
const req = request({
host: "localhost",
pathname: "test/properties",
port: coapServer.getPort(),
method: "GET",
});
req.on("response", (res: IncomingMessage) => {
expect(res.code).to.equal("4.04");
resolve();
});
req.end();
});

await coapServer.stop();
await servient.shutdown();
}

@test async "should reject unsupported methods for meta operations"() {
const coapServer = new CoapServer();
const servient = new Servient();

await coapServer.start(servient);

const testThingWithoutForms = new ExposedThing(servient, {
title: "Test",
properties: {
testInteger: {
type: "integer",
forms: [],
},
},
});

await coapServer.expose(testThingWithoutForms);

await new Promise<void>((resolve) => {
const req = request({
host: "localhost",
pathname: "test/properties",
port: coapServer.getPort(),
method: "PUT",
});
req.on("response", (res) => {
expect(res.code).to.equal("4.05");
resolve();
});
req.end();
});

await coapServer.stop();
await servient.shutdown();
}
}

0 comments on commit aae70f4

Please sign in to comment.