Skip to content

Commit

Permalink
Serve a Thing Description for the gateway - closes #2927
Browse files Browse the repository at this point in the history
  • Loading branch information
benfrancis committed Aug 15, 2024
1 parent 7aca9ee commit 3713d6e
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 30 deletions.
32 changes: 32 additions & 0 deletions src/controllers/api_root_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* API Root Controller.
*
* Handles requests to /.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import express from 'express';
import Gateway from '../models/gateway';

function build(): express.Router {
const controller = express.Router();

/**
* WoT Thing Description Directory
* https://www.w3.org/TR/wot-discovery/#exploration-directory
*/
controller.get('/', (request, response) => {
const host = request.headers.host;
const secure = request.secure;
const td = Gateway.getDescription(host, secure);
response.set('Content-type', 'application/td+json');
response.status(200).send(td);
});

return controller;
}

export default build;
28 changes: 28 additions & 0 deletions src/controllers/well-known_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

import express from 'express';
import * as Constants from '../constants';
import Gateway from '../models/gateway';

function build(): express.Router {
const controller = express.Router();

/**
* OAuth 2.0 Authorization Server Metadata (RFC 8414)
* https://datatracker.ietf.org/doc/html/rfc8414
*/
controller.get('/oauth-authorization-server', (request, response) => {
const origin = `${request.protocol}://${request.headers.host}`;
Expand All @@ -25,6 +27,32 @@ function build(): express.Router {
});
});

/**
* WoT Thing Description Directory
* https://www.w3.org/TR/wot-discovery/#exploration-directory
*/
controller.get('/wot', (request, response) => {
const host = request.headers.host;
const secure = request.secure;

// Get a Thing Description of the gateway
let td = Gateway.getDescription(host, secure);

// Add a link to root as the canonical URL of the Thing Description
if (td.links === undefined) {
td.links = [];
}
td.links.push({
rel: 'canonical',
href: '/',
type: 'application/td+json'
});

// Send the Thing Description in response
response.set('Content-type', 'application/td+json');
response.status(200).send(td);
});

return controller;
}

Expand Down
244 changes: 244 additions & 0 deletions src/models/gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/**
* Gateway Model.
*
* Represents the gateway and its interaction affordances, including
* acting as a Thing Description Directory.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import * as Constants from '../constants';
import { ThingDescription } from './thing';

export default class Gateway {

/**
*
* Get a JSON Thing Description for this gateway.
*
* @param {String} reqHost request host, if coming via HTTP
* @param {Boolean} reqSecure whether or not the request is secure, i.e. TLS
* @returns {ThingDescription} A Thing Description describing the gateway.
*/
static getDescription(reqHost?: string, reqSecure?: boolean): ThingDescription {
const origin = `${reqSecure ? 'https' : 'http'}://${reqHost}`;
const desc: ThingDescription = {
'@context': [
'https://www.w3.org/2022/wot/td/v1.1',
'https://www.w3.org/2022/wot/discovery'
],
'@type': 'ThingDirectory',
'id': origin,
'base': origin,
'title': 'WebThings Gateway',
'securityDefinitions': {
'oauth2_sc': {
'scheme': 'oauth2',
'flow': 'code',
'authorization': `${origin}${Constants.OAUTH_PATH}/authorize`,
'token': `${origin}${Constants.OAUTH_PATH}/token`,
'scopes': [Constants.THINGS_PATH, `${Constants.THINGS_PATH}:readwrite`],
}
},
'security': 'oauth2_sc',
'properties': {
'things': {
'title': 'Things',
'description': 'Retrieve all Thing Descriptions',
'type': 'array',
'items': {
'type': 'object'
},
'forms': [
{
'href': '/things',
'htv:methodName': 'GET',
'response': {
'description': 'Success response',
'htv:statusCodeValue': 200,
'contentType': 'application/json'
},
'additionalResponses': [
{
'description': 'Token must contain scope',
'htv:statusCodeValue': 400
}
]
}
]
}
},
'actions': {
'createAnonymousThing': {
'description': 'Create a Thing Description',
'input': {
'type': 'object'
},
'forms': [
{
'href': '/things',
'htv:methodName': 'POST',
'contentType': 'application/json',
'response': {
'htv:statusCodeValue': 201
},
'additionalResponses': [
{
'description': 'Invalid or duplicate Thing Description',
'htv:statusCodeValue': 400
},
{
'description': 'Internal error saving new Thing Description',
'htv:statusCodeValue': 500
}
]
}
]
},
'retrieveThing': {
'description': 'Retrieve a Thing Description',
'uriVariables': {
'id': {
'@type': 'ThingID',
'title': 'Thing Description ID',
'type': 'string',
'format': 'iri-reference'
}
},
'output': {
'description': 'The schema is implied by the content type',
'type': 'object'
},
'safe': true,
'idempotent': true,
'forms': [
{
'href': '/things/{id}',
'htv:methodName': 'GET',
'response': {
'description': 'Success response',
'htv:statusCodeValue': 200,
'contentType': 'application/json'
},
'additionalResponses': [
{
'description': 'TD with the given id not found',
'htv:statusCodeValue': 404
}
]
}
]
},
'updateThing': {
'description': 'Update a Thing Description',
'uriVariables': {
'id': {
'@type': 'ThingID',
'title': 'Thing Description ID',
'type': 'string',
'format': 'iri-reference'
}
},
'input': {
'type': 'object'
},
'forms': [
{
'href': '/things/{id}',
'htv:methodName': 'PUT',
'contentType': 'application/json',
'response': {
'description': 'Success response',
'htv:statusCodeValue': 200
},
'additionalResponses': [
{
'description': 'Invalid serialization or TD',
'htv:statusCodeValue': 400
},
{
'description': 'Failed to update Thing',
'htv:statusCodeValue': 500
}
]
}
]
},
'partiallyUpdateThing': {
'description': 'Partially update a Thing Description',
'uriVariables': {
'id': {
'@type': 'ThingID',
'title': 'Thing Description ID',
'type': 'string',
'format': 'iri-reference'
}
},
'input': {
'type': 'object'
},
'forms': [
{
'href': '/things/{id}',
'htv:methodName': 'PATCH',
'contentType': 'application/merge-patch+json',
'response': {
'description': 'Success response',
'htv:statusCodeValue': 200
},
'additionalResponses': [
{
'description': 'Request body missing required parameters',
'htv:statusCodeValue': 400
},
{
'description': 'TD with the given id not found',
'htv:statusCodeValue': 404
},
{
'description': 'Failed to update Thing',
'htv:statusCodeValue': 500
}
]
}
]
},
'deleteThing': {
'description': 'Delete a Thing Description',
'uriVariables': {
'id': {
'@type': 'ThingID',
'title': 'Thing Description ID',
'type': 'string',
'format': 'iri-reference'
}
},
'forms': [
{
'href': '/things/{id}',
'htv:methodName': 'DELETE',
'response': {
'description': 'Success response',
'htv:statusCodeValue': 204
},
'additionalResponses': [
{
'description': 'TD with the given id not found',
'htv:statusCodeValue': 404
},
{
'description': 'Failed to remove Thing',
'htv:statusCodeValue': 500
}
]
}
]
}
}
}

return desc;
}
}
Loading

0 comments on commit 3713d6e

Please sign in to comment.