From 2228da9e1bdb0704da0718316cda6731e2f1137b Mon Sep 17 00:00:00 2001 From: Ben Francis Date: Fri, 20 Mar 2026 22:42:16 +0000 Subject: [PATCH] WIP Serve minimal Thing Description - closes #4 --- src/thing.js | 129 +++++++++++++++++++++++++++++++-- src/types.d.ts | 157 ++++++++++++++++++++++++++++++++++++++++ src/validation-error.js | 33 +++++++++ tsconfig.json | 2 +- 4 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 src/types.d.ts create mode 100644 src/validation-error.js diff --git a/src/thing.js b/src/thing.js index 147e48e..31b6812 100644 --- a/src/thing.js +++ b/src/thing.js @@ -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; } /** diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..0c170e6 --- /dev/null +++ b/src/types.d.ts @@ -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; + +/** + * 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; + readOnly?: boolean; + writeOnly?: boolean; + const?: unknown; + default?: unknown; + /** Properties of an object-typed schema. */ + properties?: Record; + 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; + description?: string; + descriptions?: Record; + id?: string; + properties?: Record; + actions?: Record; + events?: Record; +} + +/** + * 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; + description?: string; + descriptions?: Record; + id?: string; + securityDefinitions: Record; + security: string | string[]; + properties?: Record; + actions?: Record; + events?: Record; + forms?: Form[]; + links?: Array<{ href: string; rel?: string; type?: string }>; +} diff --git a/src/validation-error.js b/src/validation-error.js new file mode 100644 index 0000000..40a316a --- /dev/null +++ b/src/validation-error.js @@ -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} 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; diff --git a/tsconfig.json b/tsconfig.json index c7d87c2..6f8fd40 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,5 @@ "moduleResolution": "nodenext", "target": "es2022" }, - "include": ["src/**/*.js", "examples/**/*.js"] + "include": ["src/**/*.js", "src/**/*.d.ts", "examples/**/*.js"] }