Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions examples/lamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 28 additions & 3 deletions src/property-affordance.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,27 @@ class PropertyAffordance extends InteractionAffordance {
/**
* Set read handler function.
*
* @param {function} handler A function to handle property reads.
* @param {() => Promise<any>} handler An asynchronous function to handle property reads.
*/
setReadHandler(handler) {
this.readHandler = handler;
}

/**
* Set write handler function.
*
* @param {(value: any) => Promise<void>} 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<any>} The current value of the property.
*/
read() {
async read() {
if (this.readHandler) {
return this.readHandler();
} else {
Expand All @@ -188,6 +197,22 @@ class PropertyAffordance extends InteractionAffordance {
}
}

/**
* Write the property.
*
* @param {any} value The value to write.
* @returns {Promise<void>} 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}
*/
Expand Down
35 changes: 35 additions & 0 deletions src/thing-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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();
},
);
}

/**
Expand Down
33 changes: 32 additions & 1 deletion src/thing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>} handler A function to handle property reads.
*/
setPropertyReadHandler(name, handler) {
let property = this.properties.get(name);
Expand All @@ -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<void>} 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.
*
Expand All @@ -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;
35 changes: 34 additions & 1 deletion test/thing-server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
});
});
});
62 changes: 53 additions & 9 deletions test/thing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/,
);
});
});
});
Loading