diff --git a/examples/lamp.js b/examples/lamp.js index 5cc09ba..00ca34a 100644 --- a/examples/lamp.js +++ b/examples/lamp.js @@ -57,13 +57,25 @@ const partialTD = { }; const thing = new Thing(partialTD); +let currentOnValue = false; +let currentLevelValue = 100; thing.setPropertyReadHandler('on', async function () { - return true; + return currentOnValue; }); thing.setPropertyReadHandler('level', async function () { - return 50; + return currentLevelValue; +}); + +thing.setPropertyWriteHandler('on', async function (value) { + currentOnValue = value; + return; +}); + +thing.setPropertyWriteHandler('level', async function (value) { + currentLevelValue = value; + return; }); const server = new ThingServer(thing); diff --git a/src/property-affordance.js b/src/property-affordance.js index 3bb86ff..e13f8c6 100644 --- a/src/property-affordance.js +++ b/src/property-affordance.js @@ -168,18 +168,27 @@ class PropertyAffordance extends InteractionAffordance { /** * Set read handler function. * - * @param {function} handler A function to handle property reads. + * @param {() => Promise} handler An asynchronous function to handle property reads. */ setReadHandler(handler) { this.readHandler = handler; } + /** + * Set write handler function. + * + * @param {(value: any) => Promise} handler An asynchronous function to handle property writes. + */ + setWriteHandler(handler) { + this.writeHandler = handler; + } + /** * Read the property. * - * @returns {any} The current value of the property. + * @returns {Promise} The current value of the property. */ - read() { + async read() { if (this.readHandler) { return this.readHandler(); } else { @@ -188,6 +197,22 @@ class PropertyAffordance extends InteractionAffordance { } } + /** + * Write the property. + * + * @param {any} value The value to write. + * @returns {Promise} A Promise. + */ + async write(value) { + // TODO: Check value against type in TD + if (this.writeHandler) { + return this.writeHandler(value); + } else { + console.error(`No write handler set for property ${this.name}`); + throw new Error('InternalError'); + } + } + /** * @returns {PropertyDescription} */ diff --git a/src/thing-server.js b/src/thing-server.js index f988e39..8ac0886 100644 --- a/src/thing-server.js +++ b/src/thing-server.js @@ -13,6 +13,8 @@ class ThingServer { constructor(thing) { this.thing = thing; this.app = express(); + // Use JSON middleware and allow bare primite values as valid JSON + this.app.use(express.json({ strict: false })); this.server = null; this.app.get( @@ -59,6 +61,39 @@ class ThingServer { response.status(200).json(value); }, ); + + this.app.put( + '/properties/:name', + /** + * @param {Request} request + * @param {Response} response + */ + async (request, response) => { + // Make sure name is a string since param can also be array + const name = Array.isArray(request.params.name) + ? request.params.name[0] + : request.params.name; + const value = request.body; + try { + await this.thing.writeProperty(name, value); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'InternalError'; + switch (errorMessage) { + case 'NotFoundError': + response.status(404).send(); + break; + case 'InternalError': + response.status(500).send(); + break; + default: + response.status(500).send(); + } + return; + } + response.status(204).send(); + }, + ); } /** diff --git a/src/thing.js b/src/thing.js index a8a1fc5..cb39bb3 100644 --- a/src/thing.js +++ b/src/thing.js @@ -300,7 +300,7 @@ class Thing { * Set Property Read Handler. * * @param {string} name The name of the property to handle. - * @param {function} handler A function to handle property reads. + * @param {() => Promise} handler A function to handle property reads. */ setPropertyReadHandler(name, handler) { let property = this.properties.get(name); @@ -310,6 +310,20 @@ class Thing { property.setReadHandler(handler); } + /** + * Set Property Write Handler. + * + * @param {string} name The name of the property to handle. + * @param {(value: any) => Promise} handler A function to handle property writes. + */ + setPropertyWriteHandler(name, handler) { + let property = this.properties.get(name); + if (!property) { + throw new Error(`No property called ${name} could be found`); + } + property.setWriteHandler(handler); + } + /** * Read Property. * @@ -325,6 +339,23 @@ class Thing { } return property.read(); } + + /** + * Write Property. + * + * @param {string} name The name of the property to write. + * @param {any} value The property value to write. + * @returns {any} The current value of the property, with a format conforming + * to its data schema in the Thing Description. + */ + writeProperty(name, value) { + let property = this.properties.get(name); + if (!property) { + console.error(`No property called ${name} could be found`); + throw new Error('NotFoundError'); + } + return property.write(value); + } } export default Thing; diff --git a/test/thing-server.test.js b/test/thing-server.test.js index 973e13f..3c23530 100644 --- a/test/thing-server.test.js +++ b/test/thing-server.test.js @@ -17,10 +17,14 @@ describe('ThingServer', () => { let server; let baseUrl; + let currentValue = true; before(async () => { const thing = new Thing(partialTD); - thing.setPropertyReadHandler('on', async () => true); + thing.setPropertyReadHandler('on', async () => currentValue); + thing.setPropertyWriteHandler('on', async (value) => { + currentValue = value; + }); server = new ThingServer(thing); // Listen on a random available port to avoid port conflicts await new Promise((resolve) => { @@ -65,4 +69,33 @@ describe('ThingServer', () => { assert.strictEqual(response.status, 404); }); }); + + describe('PUT /properties/:name', () => { + it('should update the property value when the property exists', async () => { + const response = await fetch(baseUrl + '/properties/on', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(false), + }); + + assert.strictEqual(response.status, 204); + assert.strictEqual(currentValue, false); + }); + }); + + describe('PUT /properties/:invalidname', () => { + it('should return 404 when the property does not exist', async () => { + const response = await fetch(baseUrl + '/properties/foo', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(true), + }); + + assert.strictEqual(response.status, 404); + }); + }); }); diff --git a/test/thing.test.js b/test/thing.test.js index 4544adc..72d79e0 100644 --- a/test/thing.test.js +++ b/test/thing.test.js @@ -50,24 +50,68 @@ describe('Thing', () => { }); }); - describe('readProperty', () => { - it('should return the value from the property read handler', () => { + describe('setPropertyReadHandler', () => { + it('should register a handler for an existing property', async () => { const thing = new Thing(partialTD); - thing.setPropertyReadHandler('on', () => true); - const value = thing.readProperty('on'); + thing.setPropertyReadHandler('on', async () => true); + const value = await thing.readProperty('on'); assert.strictEqual(value, true); }); - it('should support async property read handlers', async () => { + it('should throw when the property does not exist', () => { + const thing = new Thing(partialTD); + assert.throws( + () => thing.setPropertyReadHandler('missing', async () => {}), + /No property called missing could be found/, + ); + }); + }); + + describe('readProperty', () => { + it('should return the value from the property read handler', async () => { const thing = new Thing(partialTD); - thing.setPropertyReadHandler('on', async () => false); + thing.setPropertyReadHandler('on', async () => true); const value = await thing.readProperty('on'); - assert.strictEqual(value, false); + assert.strictEqual(value, true); + }); + + it('should reject when no handler is registered', async () => { + const thing = new Thing(partialTD); + await assert.rejects(() => thing.readProperty('on'), /InternalError/); + }); + }); + + describe('setPropertyWriteHandler', () => { + it('should register a handler for an existing property', async () => { + const thing = new Thing(partialTD); + thing.setPropertyWriteHandler('on', async (value) => value); + const value = await thing.writeProperty('on', true); + assert.strictEqual(value, true); + }); + + it('should throw when the property does not exist', () => { + const thing = new Thing(partialTD); + assert.throws( + () => thing.setPropertyWriteHandler('missing', async () => {}), + /No property called missing could be found/, + ); + }); + }); + + describe('writeProperty', () => { + it('should return the value from the property write handler', async () => { + const thing = new Thing(partialTD); + thing.setPropertyWriteHandler('on', async (value) => value); + const value = await thing.writeProperty('on', true); + assert.strictEqual(value, true); }); - it('should throw when no handler is registered', () => { + it('should reject when no handler is registered', async () => { const thing = new Thing(partialTD); - assert.throws(() => thing.readProperty('on')); + await assert.rejects( + () => thing.writeProperty('on', true), + /InternalError/, + ); }); }); });