Skip to content
Draft
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
129 changes: 123 additions & 6 deletions src/thing.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,146 @@
import ValidationError from './validation-error.js';

/**
* Thing.
*
* Represents a W3C WoT Web Thing.
*/
class Thing {
DEFAULT_CONTEXT = 'https://www.w3.org/2022/wot/td/v1.1';

propertyReadHandlers = new Map();

/**
* Construct Thing from partial Thing Description.
*
* @param {Object} partialTD A partial Thing Description two which Forms
* will be added.
* @param {import('./types.js').PartialTD} partialTD A partial Thing Description
* to which Forms will be added.
*/
constructor(partialTD) {
// TODO: Parse and validate TD.
this.partialTD = partialTD;
// Create an empty validation error to collect errors during parsing.
let validationError = new ValidationError([]);

// Parse @context member
try {
this.parseContextMember(partialTD['@context']);
} catch (error) {
if (error instanceof ValidationError) {
validationError.validationErrors.push(...error.validationErrors);
} else {
throw error;
}
}

// Parse title member
try {
this.parseTitleMember(partialTD.title);
} catch (error) {
if (error instanceof ValidationError) {
validationError.validationErrors.push(...error.validationErrors);
} else {
throw error;
}
}

// Hard code the nosec security scheme for now
this.securityDefinitions = {
nosec_sc: {
scheme: 'nosec',
},
};
this.security = 'nosec_sc';
}

/**
* Parse the @context member of a Thing Description.
*
* @param {any} context The @context, if any, provided in the partialTD.
* @throws {ValidationError} A validation error.
*/
parseContextMember(context) {
// If no @context provided then set it to the default
if (context === undefined) {
this.context = this.DEFAULT_CONTEXT;
return;
}

// If @context is a string but not the default then turn it into an Array
// and add the default as well
if (typeof context === 'string') {
if (context == this.DEFAULT_CONTEXT) {
this.context = context;
return;
} else {
this.context = new Array();
this.context.push(context);
this.context.push(this.DEFAULT_CONTEXT);
}
return;
}

// If @context is provided and it's an array but doesn't contain the default,
// then add the default
if (Array.isArray(context)) {
// TODO: Check that members of the Array are valid
this.context = context;
if (!this.context.includes(this.DEFAULT_CONTEXT)) {
this.context.push(this.DEFAULT_CONTEXT);
}
return;
}

// If @context is set but it's not a string or Array then it's invalid
throw new ValidationError([
{
field: 'title',
description: 'context member is set but is not a string or Array',
},
]);
}

/**
* Parse the title member of a Thing Description.
*
* @param {string} title The title provided in the partialTD.
* @throws {ValidationError} A validation error.
*/
parseTitleMember(title) {
// Require the user to provide a title
if (!title) {
throw new ValidationError([
{
field: '(root)',
description: 'Mandatory title member not provided',
},
]);
}

if (typeof title !== 'string') {
throw new ValidationError([
{
field: 'title',
description: 'title member is not a string',
},
]);
}

this.title = title;
}

/**
* Get Thing Description.
*
* @returns {Object} A complete Thing Description for the Thing.
* TODO: Change this return type to ThingDescription once it is valid.
*/
getThingDescription() {
// TODO: Add forms etc.
return this.partialTD;
const thingDescription = {
'@context': this.context,
title: this.title,
securityDefinitions: this.securityDefinitions,
security: this.security,
};
return thingDescription;
}

/**
Expand Down
157 changes: 157 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* TypeScript types for the W3C WoT Thing Description.
*
* @see https://www.w3.org/TR/wot-thing-description11/
*/

/**
* A JSON-LD context entry, either a URI string or a context object mapping
* prefixes to URIs.
*/
export type ContextEntry = string | Record<string, string>;

/**
* A data schema used to describe the data format of properties, action
* input/output, and event data.
*
* @see https://www.w3.org/TR/wot-thing-description11/#sec-data-schema-vocabulary-definition
*/
export interface DataSchema {
type?:
| 'boolean'
| 'integer'
| 'number'
| 'string'
| 'object'
| 'array'
| 'null';
title?: string;
description?: string;
unit?: string;
minimum?: number;
maximum?: number;
enum?: Array<string | number | boolean | null>;
readOnly?: boolean;
writeOnly?: boolean;
const?: unknown;
default?: unknown;
/** Properties of an object-typed schema. */
properties?: Record<string, DataSchema>;
required?: string[];
/** Schema for items of an array-typed schema. */
items?: DataSchema;
minItems?: number;
maxItems?: number;
oneOf?: DataSchema[];
format?: string;
}

/**
* A property affordance extending DataSchema with observable capability.
*
* @see https://www.w3.org/TR/wot-thing-description11/#propertyaffordance
*/
export interface PropertyAffordance extends DataSchema {
observable?: boolean;
}

/**
* An action affordance describing a function which can be invoked.
*
* @see https://www.w3.org/TR/wot-thing-description11/#actionaffordance
*/
export interface ActionAffordance {
title?: string;
description?: string;
input?: DataSchema;
output?: DataSchema;
safe?: boolean;
idempotent?: boolean;
synchronous?: boolean;
}

/**
* An event affordance describing an event source.
*
* @see https://www.w3.org/TR/wot-thing-description11/#eventaffordance
*/
export interface EventAffordance {
title?: string;
description?: string;
data?: DataSchema;
subscription?: DataSchema;
cancellation?: DataSchema;
}

/**
* A security scheme definition.
*
* @see https://www.w3.org/TR/wot-thing-description11/#sec-security-vocabulary-definition
*/
export interface SecurityScheme {
scheme:
| 'nosec'
| 'basic'
| 'digest'
| 'bearer'
| 'psk'
| 'oauth2'
| 'apikey'
| 'auto';
description?: string;
proxy?: string;
in?: 'header' | 'query' | 'body' | 'cookie' | 'uri' | 'auto';
name?: string;
}

/**
* A form hypermedia control describing how an operation can be performed.
*
* @see https://www.w3.org/TR/wot-thing-description11/#form
*/
export interface Form {
href: string;
contentType?: string;
op?: string | string[];
subprotocol?: string;
}

/**
* A partial Thing Description provided by the user, to which Forms and
* security metadata will be added by the server.
*
* @see https://www.w3.org/TR/wot-thing-description11/#thing
*/
export interface PartialTD {
'@context'?: ContextEntry | ContextEntry[];
title: string;
titles?: Record<string, string>;
description?: string;
descriptions?: Record<string, string>;
id?: string;
properties?: Record<string, PropertyAffordance>;
actions?: Record<string, ActionAffordance>;
events?: Record<string, EventAffordance>;
}

/**
* A complete, valid Thing Description with required security and context
* fields populated by the server.
*
* @see https://www.w3.org/TR/wot-thing-description11/#thing
*/
export interface ThingDescription {
'@context': ContextEntry | ContextEntry[];
title: string;
titles?: Record<string, string>;
description?: string;
descriptions?: Record<string, string>;
id?: string;
securityDefinitions: Record<string, SecurityScheme>;
security: string | string[];
properties?: Record<string, PropertyAffordance & { forms?: Form[] }>;
actions?: Record<string, ActionAffordance & { forms?: Form[] }>;
events?: Record<string, EventAffordance & { forms?: Form[] }>;
forms?: Form[];
links?: Array<{ href: string; rel?: string; type?: string }>;
}
33 changes: 33 additions & 0 deletions src/validation-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

/**
* Validation Error.
*
* An error containing one or more validation errors following the format
* described in the W3C WoT Discovery specification
* (https://www.w3.org/TR/wot-discovery/#exploration-directory-api-things-validation)
*/
class ValidationError extends Error {
/**
* Constructor
*
* @param {Array<Object>} validationErrors A list of validation errors, e.g.
* [
* {
* "field": "(root)",
* "description": "security is required"
* },
* {
* "field": "properties.status.forms.0.href",
* "description": "Invalid type. Expected: string, given: integer"
* }
* ]
* @param {...any} params Other Error parameters.
*/
constructor(validationErrors, ...params) {
super(...params);
this.validationErrors = validationErrors;
}
}

export default ValidationError;
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"moduleResolution": "nodenext",
"target": "es2022"
},
"include": ["src/**/*.js", "examples/**/*.js"]
"include": ["src/**/*.js", "src/**/*.d.ts", "examples/**/*.js"]
}
Loading