From 692af10d15bce1989724945026b1b29510beff3f Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Tue, 12 Sep 2023 16:25:35 +0200 Subject: [PATCH] feat(coap-server): add support for URI variables (#1078) --- packages/binding-coap/src/coap-server.ts | 49 +++++++++--- .../binding-coap/test/coap-server-test.ts | 76 ++++++++++++++++++- packages/binding-http/src/http-server.ts | 40 +--------- packages/core/src/helpers.ts | 19 ++++- 4 files changed, 135 insertions(+), 49 deletions(-) diff --git a/packages/binding-coap/src/coap-server.ts b/packages/binding-coap/src/coap-server.ts index 04ecb5d3f..e2acd028e 100644 --- a/packages/binding-coap/src/coap-server.ts +++ b/packages/binding-coap/src/coap-server.ts @@ -189,7 +189,9 @@ export default class CoapServer implements ProtocolServer { this.PROPERTY_DIR, propertyName, offeredMediaType, - opValues + opValues, + property.uriVariables, + thing.uriVariables ); property.forms.push(form); @@ -204,7 +206,9 @@ export default class CoapServer implements ProtocolServer { this.ACTION_DIR, actionName, offeredMediaType, - "invokeaction" + "invokeaction", + action.uriVariables, + thing.uriVariables ); action.forms.push(form); @@ -215,10 +219,15 @@ export default class CoapServer implements ProtocolServer { private fillInEventBindingData(thing: ExposedThing, base: string, port: number, offeredMediaType: string) { for (const [eventName, event] of Object.entries(thing.events)) { - const [href, form] = this.createHrefAndForm(base, this.EVENT_DIR, eventName, offeredMediaType, [ - "subscribeevent", - "unsubscribeevent", - ]); + const [href, form] = this.createHrefAndForm( + base, + this.EVENT_DIR, + eventName, + offeredMediaType, + ["subscribeevent", "unsubscribeevent"], + event.uriVariables, + thing.uriVariables + ); event.forms.push(form); @@ -231,16 +240,36 @@ export default class CoapServer implements ProtocolServer { affordancePathSegment: string, affordanceName: string, offeredMediaType: string, - opValues: string | string[] + opValues: string | string[], + affordanceUriVariables: PropertyElement["uriVariables"] = {}, + thingUriVariables: PropertyElement["uriVariables"] = {} ): [string, TD.Form] { - const href = this.createFormHref(base, affordancePathSegment, affordanceName); + const href = this.createFormHref( + base, + affordancePathSegment, + affordanceName, + affordanceUriVariables, + thingUriVariables + ); const form = this.createAffordanceForm(href, offeredMediaType, opValues); return [href, form]; } - private createFormHref(base: string, affordancePathSegment: string, affordanceName: string) { - return `${base}/${affordancePathSegment}/${encodeURIComponent(affordanceName)}`; + private createFormHref( + base: string, + affordancePathSegment: string, + affordanceName: string, + affordanceUriVariables: PropertyElement["uriVariables"] = {}, + thingUriVariables: PropertyElement["uriVariables"] = {} + ) { + const affordanceNamePattern = Helpers.updateInteractionNameWithUriVariablePattern( + affordanceName, + affordanceUriVariables, + thingUriVariables + ); + + return `${base}/${affordancePathSegment}/${encodeURIComponent(affordanceNamePattern)}`; } private createAffordanceForm(href: string, offeredMediaType: string, op: string[] | string) { diff --git a/packages/binding-coap/test/coap-server-test.ts b/packages/binding-coap/test/coap-server-test.ts index a7e0057e1..8364cc91f 100644 --- a/packages/binding-coap/test/coap-server-test.ts +++ b/packages/binding-coap/test/coap-server-test.ts @@ -20,7 +20,7 @@ import Servient, { ExposedThing, Content } from "@node-wot/core"; import { suite, test } from "@testdeck/mocha"; import { expect, should } from "chai"; -import { DataSchemaValue, InteractionInput } from "wot-typescript-definitions"; +import { DataSchemaValue, InteractionInput, InteractionOptions } from "wot-typescript-definitions"; import * as TD from "@node-wot/td-tools"; import CoapServer from "../src/coap-server"; import { CoapClient } from "../src/coap"; @@ -389,4 +389,78 @@ class CoapServerTest { }); req.end(); } + + @test async "should check uriVariables consistency"() { + const portNumber = 9003; + const coapServer = new CoapServer(portNumber); + const servient = new Servient(); + + const baseUri = `coap://localhost:${portNumber}/test`; + + await coapServer.start(servient); + + const testThing = new ExposedThing(servient, { + title: "Test", + properties: { + test: { + type: "string", + uriVariables: { + id: { + type: "string", + }, + }, + }, + }, + actions: { + try: { + output: { type: "string" }, + uriVariables: { + step: { type: "integer" }, + }, + }, + }, + }); + + let test: DataSchemaValue; + testThing.setPropertyReadHandler("test", (options) => { + expect(options?.uriVariables).to.deep.equal({ id: "testId" }); + return new Promise((resolve, reject) => { + resolve(test); + }); + }); + testThing.setPropertyWriteHandler("test", async (value, options) => { + expect(options?.uriVariables).to.deep.equal({ id: "testId" }); + test = await value.value(); + expect(test?.valueOf()).to.deep.equal("on"); + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + testThing.properties.test.forms = []; + testThing.setActionHandler("try", (input: WoT.InteractionOutput, params?: InteractionOptions) => { + return new Promise((resolve, reject) => { + expect(params?.uriVariables).to.deep.equal({ step: 5 }); + resolve("TEST"); + }); + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + testThing.actions.try.forms = []; + + await coapServer.expose(testThing); + + const coapClient = new CoapClient(coapServer); + + const propertyUri = `${baseUri}/properties/test?id=testId`; + + await coapClient.writeResource(new TD.Form(propertyUri), new Content("text/plain", Readable.from("on"))); + + const response1 = await coapClient.readResource(new TD.Form(propertyUri)); + expect((await response1.toBuffer()).toString()).to.equal('"on"'); + + const response2 = await coapClient.invokeResource(new TD.Form(`${baseUri}/actions/try?step=5`)); + expect((await response2.toBuffer()).toString()).to.equal('"TEST"'); + + await coapClient.stop(); + await coapServer.stop(); + } } diff --git a/packages/binding-http/src/http-server.ts b/packages/binding-http/src/http-server.ts index 772925cb6..aa5b5422b 100644 --- a/packages/binding-http/src/http-server.ts +++ b/packages/binding-http/src/http-server.ts @@ -261,40 +261,6 @@ export default class HttpServer implements ProtocolServer { } } - private updateInteractionNameWithUriVariablePattern( - interactionName: string, - uriVariables: PropertyElement["uriVariables"] = {}, - thingVariables: PropertyElement["uriVariables"] = {} - ): string { - const variables = Object.assign({}, uriVariables, thingVariables); - if (Object.keys(variables).length > 0) { - let pattern = "{?"; - let index = 0; - if (uriVariables) { - for (const key in uriVariables) { - if (index !== 0) { - pattern += ","; - } - pattern += encodeURIComponent(key); - index++; - } - } - if (thingVariables) { - for (const key in thingVariables) { - if (index !== 0) { - pattern += ","; - } - pattern += encodeURIComponent(key); - index++; - } - } - pattern += "}"; - return encodeURIComponent(interactionName) + pattern; - } else { - return encodeURIComponent(interactionName); - } - } - public async expose(thing: ExposedThing, tdTemplate: WoT.ExposedThingInit = {}): Promise { let urlPath = slugify(thing.title, { lower: true }); @@ -400,7 +366,7 @@ export default class HttpServer implements ProtocolServer { } for (const propertyName in thing.properties) { - const propertyNamePattern = this.updateInteractionNameWithUriVariablePattern( + const propertyNamePattern = Helpers.updateInteractionNameWithUriVariablePattern( propertyName, thing.properties[propertyName].uriVariables, thing.uriVariables @@ -453,7 +419,7 @@ export default class HttpServer implements ProtocolServer { } for (const actionName in thing.actions) { - const actionNamePattern = this.updateInteractionNameWithUriVariablePattern( + const actionNamePattern = Helpers.updateInteractionNameWithUriVariablePattern( actionName, thing.actions[actionName].uriVariables, thing.uriVariables @@ -475,7 +441,7 @@ export default class HttpServer implements ProtocolServer { } for (const eventName in thing.events) { - const eventNamePattern = this.updateInteractionNameWithUriVariablePattern( + const eventNamePattern = Helpers.updateInteractionNameWithUriVariablePattern( eventName, thing.events[eventName].uriVariables, thing.uriVariables diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 46a0211d9..48d9ca26c 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -38,7 +38,7 @@ import { DataSchemaValue, ExposedThingInit } from "wot-typescript-definitions"; import { SomeJSONSchema } from "ajv/dist/types/json-schema"; import { ThingInteraction, ThingModelHelpers } from "@node-wot/td-tools"; import { Resolver } from "@node-wot/td-tools/src/resolver-interface"; -import { DataSchema } from "wot-thing-description-types"; +import { PropertyElement, DataSchema } from "wot-thing-description-types"; import { createLoggers } from "./logger"; const { debug, error, warn } = createLoggers("core", "helpers"); @@ -386,4 +386,21 @@ export default class Helpers implements Resolver { return params; } + + public static updateInteractionNameWithUriVariablePattern( + interactionName: string, + affordanceUriVariables: PropertyElement["uriVariables"] = {}, + thingUriVariables: PropertyElement["uriVariables"] = {} + ): string { + const encodedInteractionName = encodeURIComponent(interactionName); + const uriVariables = [...Object.keys(affordanceUriVariables), ...Object.keys(thingUriVariables)]; + + if (uriVariables.length === 0) { + return encodedInteractionName; + } + + const pattern = uriVariables.map(encodeURIComponent).join(","); + + return encodedInteractionName + "{?" + pattern + "}"; + } }