Skip to content

Commit a9c2f5d

Browse files
committed
Serve minimal Thing Description - closes #4
1 parent 4728cf8 commit a9c2f5d

5 files changed

Lines changed: 188 additions & 12 deletions

File tree

src/server.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ class ThingServer {
1919

2020
this.app.get('/properties/:name', async (request, response) => {
2121
const name = request.params.name;
22-
const value = await this.thing.readProperty(name);
22+
let value;
23+
try {
24+
value = await this.thing.readProperty(name);
25+
} catch {
26+
response.status(404).send();
27+
return;
28+
}
2329
response.status(200).json(value);
2430
});
2531
}

src/thing.js

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,130 @@
1+
import ValidationError from './validation-error.js';
2+
13
/**
24
* Thing.
35
*
46
* Represents a W3C WoT Web Thing.
57
*/
68
class Thing {
9+
DEFAULT_CONTEXT = 'https://www.w3.org/2022/wot/td/v1.1';
10+
711
propertyReadHandlers = new Map();
812

913
/**
14+
* Construct Thing from partial Thing Description.
1015
*
11-
* @param {Object} partialTD A partial Thing Description two which Forms
12-
* will be added.
16+
* @param {Record<string, any>} partialTD A partial Thing Description
17+
* to which Forms will be added.
1318
*/
1419
constructor(partialTD) {
15-
// TODO: Parse and validate TD.
16-
this.partialTD = partialTD;
20+
// Create an empty validation error to collect errors during parsing.
21+
let validationError = new ValidationError([]);
22+
23+
// Parse @context member
24+
try {
25+
this.parseContextMember(partialTD['@context']);
26+
} catch (error) {
27+
if (error instanceof ValidationError) {
28+
validationError.validationErrors.push(...error.validationErrors);
29+
} else {
30+
throw error;
31+
}
32+
}
33+
34+
// Parse title member
35+
try {
36+
this.parseTitleMember(partialTD.title);
37+
} catch (error) {
38+
if (error instanceof ValidationError) {
39+
validationError.validationErrors.push(...error.validationErrors);
40+
} else {
41+
throw error;
42+
}
43+
}
44+
45+
// Hard code the nosec security scheme for now
46+
this.securityDefinitions = {
47+
nosec_sc: {
48+
scheme: 'nosec',
49+
},
50+
};
51+
this.security = 'nosec_sc';
52+
}
53+
54+
/**
55+
* Parse the @context member of a Thing Description.
56+
*
57+
* @param {any} context The @context, if any, provided in the partialTD.
58+
* @throws {ValidationError} A validation error.
59+
*/
60+
parseContextMember(context) {
61+
// If no @context provided then set it to the default
62+
if (context === undefined) {
63+
this.context = this.DEFAULT_CONTEXT;
64+
return;
65+
}
66+
67+
// If @context is a string but not the default then turn it into an Array
68+
// and add the default as well
69+
if (typeof context === 'string') {
70+
if (context == this.DEFAULT_CONTEXT) {
71+
this.context = context;
72+
return;
73+
} else {
74+
this.context = new Array();
75+
this.context.push(context);
76+
this.context.push(this.DEFAULT_CONTEXT);
77+
}
78+
return;
79+
}
80+
81+
// If @context is provided and it's an array but doesn't contain the default,
82+
// then add the default
83+
if (Array.isArray(context)) {
84+
// TODO: Check that members of the Array are valid
85+
this.context = context;
86+
if (!this.context.includes(this.DEFAULT_CONTEXT)) {
87+
this.context.push(this.DEFAULT_CONTEXT);
88+
}
89+
return;
90+
}
91+
92+
// If @context is set but it's not a string or Array then it's invalid
93+
throw new ValidationError([
94+
{
95+
field: 'title',
96+
description: 'context member is set but is not a string or Array',
97+
},
98+
]);
99+
}
100+
101+
/**
102+
* Parse the title member of a Thing Description.
103+
*
104+
* @param {string} title The title provided in the partialTD.
105+
* @throws {ValidationError} A validation error.
106+
*/
107+
parseTitleMember(title) {
108+
// Require the user to provide a title
109+
if (!title) {
110+
throw new ValidationError([
111+
{
112+
field: '(root)',
113+
description: 'Mandatory title member not provided',
114+
},
115+
]);
116+
}
117+
118+
if (typeof title !== 'string') {
119+
throw new ValidationError([
120+
{
121+
field: 'title',
122+
description: 'title member is not a string',
123+
},
124+
]);
125+
}
126+
127+
this.title = title;
17128
}
18129

19130
/**
@@ -22,8 +133,13 @@ class Thing {
22133
* @returns {Object} A complete Thing Description for the Thing.
23134
*/
24135
getThingDescription() {
25-
// TODO: Add forms etc.
26-
return this.partialTD;
136+
const thingDescription = {
137+
'@context': this.context,
138+
title: this.title,
139+
securityDefinitions: this.securityDefinitions,
140+
security: this.security,
141+
};
142+
return thingDescription;
27143
}
28144

29145
/**

src/validation-error.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
/**
4+
* Validation Error.
5+
*
6+
* An error containing one or more validation errors following the format
7+
* described in the W3C WoT Discovery specification
8+
* (https://www.w3.org/TR/wot-discovery/#exploration-directory-api-things-validation)
9+
*/
10+
class ValidationError extends Error {
11+
/**
12+
* Constructor
13+
*
14+
* @param {Array<Object>} validationErrors A list of validation errors, e.g.
15+
* [
16+
* {
17+
* "field": "(root)",
18+
* "description": "security is required"
19+
* },
20+
* {
21+
* "field": "properties.status.forms.0.href",
22+
* "description": "Invalid type. Expected: string, given: integer"
23+
* }
24+
* ]
25+
* @param {...any} params Other Error parameters.
26+
*/
27+
constructor(validationErrors, ...params) {
28+
super(...params);
29+
this.validationErrors = validationErrors;
30+
}
31+
}
32+
33+
export default ValidationError;

test/server.test.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('ThingServer', () => {
2222
const thing = new Thing(partialTD);
2323
thing.setPropertyReadHandler('on', async () => true);
2424
server = new ThingServer(thing);
25+
// Listen on a random available port to avoid port conflicts
2526
await new Promise((resolve) => {
2627
server.server = server.app.listen(0, () => {
2728
const port = server.server.address().port;
@@ -41,8 +42,11 @@ describe('ThingServer', () => {
4142
assert.strictEqual(response.status, 200);
4243
const td = await response.json();
4344
assert.strictEqual(td.title, 'Test Lamp');
44-
assert.strictEqual(td.description, 'A test lamp');
45-
assert.ok(td.properties);
45+
assert.equal(td['@context'], 'https://www.w3.org/2022/wot/td/v1.1');
46+
assert.deepEqual(td.securityDefinitions, {
47+
nosec_sc: { scheme: 'nosec' },
48+
});
49+
assert.equal(td.security, 'nosec_sc');
4650
});
4751
});
4852

@@ -54,4 +58,11 @@ describe('ThingServer', () => {
5458
assert.strictEqual(value, true);
5559
});
5660
});
61+
62+
describe('GET /properties/:invalidname', () => {
63+
it('should return 404 for an invalid property name', async () => {
64+
const response = await fetch(baseUrl + '/properties/foo');
65+
assert.strictEqual(response.status, 404);
66+
});
67+
});
5768
});

test/thing.test.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,27 @@ describe('Thing', () => {
1515
};
1616

1717
describe('constructor', () => {
18-
it('should store the partial Thing Description', () => {
18+
it('should parse and populate mandatory members of the Thing', () => {
1919
const thing = new Thing(partialTD);
20-
assert.deepStrictEqual(thing.partialTD, partialTD);
20+
assert.equal(thing.context, 'https://www.w3.org/2022/wot/td/v1.1');
21+
assert.equal(thing.title, partialTD.title);
22+
assert.deepEqual(thing.securityDefinitions, {
23+
nosec_sc: { scheme: 'nosec' },
24+
});
25+
assert.equal(thing.security, 'nosec_sc');
2126
});
2227
});
2328

2429
describe('getThingDescription', () => {
2530
it('should return the Thing Description', () => {
2631
const thing = new Thing(partialTD);
2732
const td = thing.getThingDescription();
28-
assert.deepStrictEqual(td, partialTD);
33+
assert.deepEqual(td, {
34+
'@context': 'https://www.w3.org/2022/wot/td/v1.1',
35+
title: 'Test Thing',
36+
securityDefinitions: { nosec_sc: { scheme: 'nosec' } },
37+
security: 'nosec_sc',
38+
});
2939
});
3040
});
3141

0 commit comments

Comments
 (0)