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 16, 2024
1 parent 7aca9ee commit 61f9848
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 29 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
const td = Gateway.getDescription(host, secure);

// Add a link to root as the canonical URL of the Thing Description
if (typeof 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
240 changes: 240 additions & 0 deletions src/models/gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**
* 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 61f9848

Please sign in to comment.