diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index ebfb8ce4c..445373ae8 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -558,7 +558,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { const content = await client.readResource(form); try { - return this.handleInteractionOutput(content, form, tp); + return this.handleInteractionOutput(content, form, tp, false); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); throw new Error(`Error while processing property for ${tp.title}. ${error.message}`); @@ -568,7 +568,8 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { private handleInteractionOutput( content: Content, form: TD.Form, - outputDataSchema: WoT.DataSchema | undefined + outputDataSchema: WoT.DataSchema | undefined, + ignoreValidation: boolean ): InteractionOutput { // infer media type from form if not in response metadata content.type ??= form.contentType ?? "application/json"; @@ -583,7 +584,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { ); } } - return new InteractionOutput(content, form, outputDataSchema); + return new InteractionOutput(content, form, outputDataSchema, { ignoreValidation }); } async _readProperties(propertyNames: string[]): Promise { @@ -703,7 +704,8 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { const content = await client.invokeResource(form, input); try { - return this.handleInteractionOutput(content, form, ta.output); + const ignoreValidation = ta.synchronous === undefined ? true : !ta.synchronous; + return this.handleInteractionOutput(content, form, ta.output, ignoreValidation); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); throw new Error(`Error while processing action for ${ta.title}. ${error.message}`); @@ -746,7 +748,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { // next (content) => { try { - listener(this.handleInteractionOutput(content, form, tp)); + listener(this.handleInteractionOutput(content, form, tp, false)); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); warn(`Error while processing observe property for ${tp.title}. ${error.message}`); @@ -802,7 +804,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { formWithoutURITemplates, (content) => { try { - listener(this.handleInteractionOutput(content, form, te.data)); + listener(this.handleInteractionOutput(content, form, te.data, false)); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); warn(`Error while processing event for ${te.title}. ${error.message}`); diff --git a/packages/core/src/interaction-output.ts b/packages/core/src/interaction-output.ts index e5b6a4b59..2d6080a10 100644 --- a/packages/core/src/interaction-output.ts +++ b/packages/core/src/interaction-output.ts @@ -42,6 +42,7 @@ export class InteractionOutput implements WoT.InteractionOutput { dataUsed: boolean; form?: WoT.Form; schema?: WoT.DataSchema; + ignoreValidation: boolean; // by default set to false public get data(): ReadableStream { if (this.#stream) { @@ -57,10 +58,11 @@ export class InteractionOutput implements WoT.InteractionOutput { return (this.#stream = ProtocolHelpers.toWoTStream(this.#content.body) as ReadableStream); } - constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema) { + constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema, options = { ignoreValidation: false }) { this.#content = content; this.form = form; this.schema = schema; + this.ignoreValidation = options.ignoreValidation ?? false; this.dataUsed = false; } @@ -122,7 +124,7 @@ export class InteractionOutput implements WoT.InteractionOutput { // validate the schema const validate = ajv.compile(this.schema); - if (!validate(json)) { + if (!this.ignoreValidation && !validate(json)) { debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`); debug(`value: ${json}`); debug(`Error: ${validate.errors}`); @@ -130,6 +132,6 @@ export class InteractionOutput implements WoT.InteractionOutput { } this.#value = json; - return json; + return json as T; } } diff --git a/packages/core/test/ClientTest.ts b/packages/core/test/ClientTest.ts index f13e5f829..ec7bfecac 100644 --- a/packages/core/test/ClientTest.ts +++ b/packages/core/test/ClientTest.ts @@ -108,6 +108,18 @@ const myThingDesc = { }, ], }, + anAsyncAction: { + input: { type: "integer" }, + output: { type: "integer" }, + synchronous: false, + forms: [ + { + href: "testdata://host/athing/actions/anasyncaction", + mediaType: "application/json", + response: { contentType: "application/json" }, + }, + ], + }, }, events: { anEvent: { @@ -510,6 +522,28 @@ class WoTClientTest { } } + @test async "call an async action"() { + // should not throw Error: Invalid value according to DataSchema + WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => { + const valueData = await content.toBuffer(); + expect(valueData.toString()).to.equal("23"); + return new Content("application/json", Readable.from(Buffer.from(JSON.stringify({ status: "pending" })))); + }); + const td = (await WoTClientTest.WoTHelpers.fetch("td://foo")) as ThingDescription; + + const thing = await WoTClientTest.WoT.consume(td); + + expect(thing).to.have.property("title").that.equals("aThing"); + expect(thing).to.have.property("actions").that.has.property("anAction"); + + // deal with ActionStatus object + const result = await thing.invokeAction("anAsyncAction", 23); + // eslint-disable-next-line no-unused-expressions + expect(result).not.to.be.null; + const value = await result?.value(); + expect(value).to.have.property("status"); + } + @test async "subscribe to event"() { WoTClientTest.clientFactory.setTrap(() => { return new Content("application/json", Readable.from(Buffer.from("triggered"))); diff --git a/packages/core/test/InteractionOutputTest.ts b/packages/core/test/InteractionOutputTest.ts index 0d66b39dd..931317cbe 100644 --- a/packages/core/test/InteractionOutputTest.ts +++ b/packages/core/test/InteractionOutputTest.ts @@ -20,6 +20,7 @@ import { expect, use } from "chai"; import { Readable } from "stream"; import { InteractionOutput } from "../src/interaction-output"; import { Content } from ".."; +import { fail } from "assert"; use(promised); const delay = (ms: number) => { @@ -106,6 +107,30 @@ class InteractionOutputTests { expect(result).be.true; } + @test async "should fail returning unexpected value with no validation"() { + const stream = Readable.from(Buffer.from("not boolean", "utf-8")); + const content = new Content("application/json", stream); + + const out = new InteractionOutput(content, {}, { type: "boolean" }); // ignoreValidation false by default + try { + const result = await out.value(); + expect(result).be.true; + fail("Wrongly allows invalid value"); + } catch { + // expected to throw + } + } + + @test async "should accept returning unexpected value with no validation"() { + // type boolean should not throw since we set ignoreValidation to true + const stream = Readable.from(Buffer.from("not boolean", "utf-8")); + const content = new Content("application/json", stream); + + const out = new InteractionOutput(content, {}, { type: "boolean" }, { ignoreValidation: true }); + const result = await out.value(); + expect(result).to.eql("not boolean"); + } + @test async "should data be used after arrayBuffer"() { const stream = Readable.from(Buffer.from("true", "utf-8")); const content = new Content("application/json", stream);