From e643b479ec42b49e08eb3e56902a7677b05e4a23 Mon Sep 17 00:00:00 2001 From: Ben Francis Date: Fri, 11 Sep 2026 22:59:14 +0100 Subject: [PATCH] WIP HTTP invokeaction implementation - closes #7 --- src/action-affordance.js | 148 +++++++++++++++++++++++++++++++++++++++ src/types.js | 54 ++++++++++---- src/validation-error.js | 13 ++++ 3 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 src/action-affordance.js diff --git a/src/action-affordance.js b/src/action-affordance.js new file mode 100644 index 0000000..2d8f982 --- /dev/null +++ b/src/action-affordance.js @@ -0,0 +1,148 @@ +import InteractionAffordance from './interaction-affordance.js'; +import ValidationError from './validation-error.js'; + +/** @import {DataSchema, PartialActionDescription, ActionDescription, Form} from "./types.js" */ + +/** + * Action Affordance + * + * Represents an ActionAffordance from the W3C WoT Thing Description 1.1 + * specification https://www.w3.org/TR/wot-thing-description/#actionaffordance + */ +class ActionAffordance extends InteractionAffordance { + /** + * @type {DataSchema|undefined} + */ + input; + + /** + * @type {DataSchema|undefined} + */ + output; + + /** + * @type {boolean|undefined} + */ + safe; + + /** + * @type {boolean|undefined} + */ + idempotent; + + /** + * @type {boolean|undefined} + */ + synchronous; + + /** + * Create a new Action. + * + * @param {string} name The name of the ActionAffordance from its + * key in an actions Map. + * @param {PartialActionDescription} metadata Metadata describing an + * ActionAffordance from a partial Thing Description. + */ + constructor(name, metadata) { + super(name, metadata); + + let validationError = new ValidationError([]); + + // Parse safe member + try { + this.#parseSafeMember(metadata.safe); + } catch (error) { + validationError.merge(error); + } + + // Parse idempotent member + try { + this.#parseIdempotentMember(metadata.idempotent); + } catch (error) { + validationError.merge(error); + } + + // TODO: Parse other members + } + + /** + * Parse safe member. + * + * @param {boolean|undefined} safe + * + * TODO: Consider omitting this member if not specified + */ + #parseSafeMember(safe) { + // Throw an error if not a boolean or undefined + if (!(safe === undefined || typeof safe == 'boolean')) { + throw new ValidationError([ + { + field: `actions.${this.name}.safe`, + description: 'safe member is not a boolean', + }, + ]); + } + // If undefined then default to false + if (safe === undefined) { + this.safe = false; + // Otherwise set the provided value + } else { + this.safe = safe; + } + } + + /** + * Parse idempotent member. + * + * @param {boolean|undefined} idempotent + * + * TODO: Consider omitting this member if not specified + */ + #parseIdempotentMember(idempotent) { + // Throw an error if not a boolean or undefined + if (!(idempotent === undefined || typeof idempotent == 'boolean')) { + throw new ValidationError([ + { + field: `actions.${this.name}.idempotent`, + description: 'idempotent member is not a boolean', + }, + ]); + } + // If undefined then default to false + if (idempotent === undefined) { + this.idempotent = false; + // Otherwise set the provided value + } else { + this.idempotent = idempotent; + } + } + + /** + * Set invoke handler function. + * + * @param {(value: any) => Promise} handler An asynchronous function to action invocations. + */ + setInvokeHandler(handler) { + this.invokeHandler = handler; + } + + /** + * Invoke the action. + * + * @param {any} input The input to the action. + * @returns {Promise} A Promise which resolves with the output of the action. + */ + async invoke(input) { + // TODO + } + + /** + * @returns {ActionDescription} + */ + getMetadata() { + //TODO + return { forms: [{ href: 'http://localhost' }] }; + } +} + +export default ActionAffordance; diff --git a/src/types.js b/src/types.js index 732419f..ade6fa5 100644 --- a/src/types.js +++ b/src/types.js @@ -67,15 +67,19 @@ // TODO: Constrain set of possible values for op /** - * Property Description - * * @typedef {{ * '@type'?: string|Array, - * title?: string|undefined, - * description?: string|undefined, + * title?: string, + * description?: string + * }} InteractionDescription + */ + +/** + * Property Description + * + * @typedef {InteractionDescription & DataSchema & { * forms: Array
, - * readOnly?: boolean, - * writeOnly?: boolean + * observeable?: boolean|undefined * }} PropertyDescription */ @@ -84,16 +88,40 @@ * * Same as PropertyDescription but forms is optional * - * @typedef {{ - * '@type'?: string|Array, - * title?: string|undefined, - * description?: string|undefined, + * @typedef {InteractionDescription & DataSchema & { * forms?: Array, - * readOnly?: boolean, - * writeOnly?: boolean + * observeable?: boolean|undefined * }} PartialPropertyDescription */ +/** + * Action Description + * + * @typedef { InteractionDescription & { + * forms: Array, + * input?: DataSchema, + * output?: DataSchema, + * safe?: boolean, + * idempotent?: boolean, + * synchronous?: boolean, + * }} ActionDescription + */ + +/** + * Partial Action Description + * + * Same as ActionDescription but forms is optional + * + * @typedef { InteractionDescription & { + * forms?: Array, + * input?: DataSchema, + * output?: DataSchema, + * safe?: boolean, + * idempotent?: boolean, + * synchronous?: boolean, + * }} PartialActionDescription + */ + /** * Thing Description * @@ -105,6 +133,7 @@ * description?: string, * base?: string, * properties?: Record, + * actions?: Record, * security: string|Array, * securityDefinitions: string|Record * }} ThingDescription @@ -124,6 +153,7 @@ * description?: string, * base?: string, * properties?: Record, + * actions?: Record, * security?: string|Array, * securityDefinitions?: string|Record * }} PartialThingDescription diff --git a/src/validation-error.js b/src/validation-error.js index 40a316a..cddb395 100644 --- a/src/validation-error.js +++ b/src/validation-error.js @@ -28,6 +28,19 @@ class ValidationError extends Error { super(...params); this.validationErrors = validationErrors; } + + /** + * Merge a list of validation errors into this ValidationError. + * + * @param {any} error + */ + merge(error) { + if (error instanceof ValidationError) { + this.validationErrors.push(...error.validationErrors); + } else { + throw error; + } + } } export default ValidationError;