Skip to content

Commit 3cbd73c

Browse files
authored
Merge pull request #1279 from danielpeintner/issue-1278
refactor: handle "non"-validating output for async actions
2 parents 5c17b56 + f14ae6b commit 3cbd73c

File tree

4 files changed

+72
-9
lines changed

4 files changed

+72
-9
lines changed

packages/core/src/consumed-thing.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
558558

559559
const content = await client.readResource(form);
560560
try {
561-
return this.handleInteractionOutput(content, form, tp);
561+
return this.handleInteractionOutput(content, form, tp, false);
562562
} catch (e) {
563563
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
564564
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 {
568568
private handleInteractionOutput(
569569
content: Content,
570570
form: TD.Form,
571-
outputDataSchema: WoT.DataSchema | undefined
571+
outputDataSchema: WoT.DataSchema | undefined,
572+
ignoreValidation: boolean
572573
): InteractionOutput {
573574
// infer media type from form if not in response metadata
574575
content.type ??= form.contentType ?? "application/json";
@@ -583,7 +584,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
583584
);
584585
}
585586
}
586-
return new InteractionOutput(content, form, outputDataSchema);
587+
return new InteractionOutput(content, form, outputDataSchema, { ignoreValidation });
587588
}
588589

589590
async _readProperties(propertyNames: string[]): Promise<WoT.PropertyReadMap> {
@@ -703,7 +704,8 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
703704

704705
const content = await client.invokeResource(form, input);
705706
try {
706-
return this.handleInteractionOutput(content, form, ta.output);
707+
const ignoreValidation = ta.synchronous === undefined ? true : !ta.synchronous;
708+
return this.handleInteractionOutput(content, form, ta.output, ignoreValidation);
707709
} catch (e) {
708710
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
709711
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 {
746748
// next
747749
(content) => {
748750
try {
749-
listener(this.handleInteractionOutput(content, form, tp));
751+
listener(this.handleInteractionOutput(content, form, tp, false));
750752
} catch (e) {
751753
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
752754
warn(`Error while processing observe property for ${tp.title}. ${error.message}`);
@@ -802,7 +804,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
802804
formWithoutURITemplates,
803805
(content) => {
804806
try {
805-
listener(this.handleInteractionOutput(content, form, te.data));
807+
listener(this.handleInteractionOutput(content, form, te.data, false));
806808
} catch (e) {
807809
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
808810
warn(`Error while processing event for ${te.title}. ${error.message}`);

packages/core/src/interaction-output.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class InteractionOutput implements WoT.InteractionOutput {
4242
dataUsed: boolean;
4343
form?: WoT.Form;
4444
schema?: WoT.DataSchema;
45+
ignoreValidation: boolean; // by default set to false
4546

4647
public get data(): ReadableStream {
4748
if (this.#stream) {
@@ -57,10 +58,11 @@ export class InteractionOutput implements WoT.InteractionOutput {
5758
return (this.#stream = ProtocolHelpers.toWoTStream(this.#content.body) as ReadableStream);
5859
}
5960

60-
constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema) {
61+
constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema, options = { ignoreValidation: false }) {
6162
this.#content = content;
6263
this.form = form;
6364
this.schema = schema;
65+
this.ignoreValidation = options.ignoreValidation ?? false;
6466
this.dataUsed = false;
6567
}
6668

@@ -122,14 +124,14 @@ export class InteractionOutput implements WoT.InteractionOutput {
122124
// validate the schema
123125
const validate = ajv.compile<T>(this.schema);
124126

125-
if (!validate(json)) {
127+
if (!this.ignoreValidation && !validate(json)) {
126128
debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`);
127129
debug(`value: ${json}`);
128130
debug(`Error: ${validate.errors}`);
129131
throw new DataSchemaError("Invalid value according to DataSchema", json as WoT.DataSchemaValue);
130132
}
131133

132134
this.#value = json;
133-
return json;
135+
return json as T;
134136
}
135137
}

packages/core/test/ClientTest.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,18 @@ const myThingDesc = {
108108
},
109109
],
110110
},
111+
anAsyncAction: {
112+
input: { type: "integer" },
113+
output: { type: "integer" },
114+
synchronous: false,
115+
forms: [
116+
{
117+
href: "testdata://host/athing/actions/anasyncaction",
118+
mediaType: "application/json",
119+
response: { contentType: "application/json" },
120+
},
121+
],
122+
},
111123
},
112124
events: {
113125
anEvent: {
@@ -510,6 +522,28 @@ class WoTClientTest {
510522
}
511523
}
512524

525+
@test async "call an async action"() {
526+
// should not throw Error: Invalid value according to DataSchema
527+
WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => {
528+
const valueData = await content.toBuffer();
529+
expect(valueData.toString()).to.equal("23");
530+
return new Content("application/json", Readable.from(Buffer.from(JSON.stringify({ status: "pending" }))));
531+
});
532+
const td = (await WoTClientTest.WoTHelpers.fetch("td://foo")) as ThingDescription;
533+
534+
const thing = await WoTClientTest.WoT.consume(td);
535+
536+
expect(thing).to.have.property("title").that.equals("aThing");
537+
expect(thing).to.have.property("actions").that.has.property("anAction");
538+
539+
// deal with ActionStatus object
540+
const result = await thing.invokeAction("anAsyncAction", 23);
541+
// eslint-disable-next-line no-unused-expressions
542+
expect(result).not.to.be.null;
543+
const value = await result?.value();
544+
expect(value).to.have.property("status");
545+
}
546+
513547
@test async "subscribe to event"() {
514548
WoTClientTest.clientFactory.setTrap(() => {
515549
return new Content("application/json", Readable.from(Buffer.from("triggered")));

packages/core/test/InteractionOutputTest.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { expect, use } from "chai";
2020
import { Readable } from "stream";
2121
import { InteractionOutput } from "../src/interaction-output";
2222
import { Content } from "..";
23+
import { fail } from "assert";
2324

2425
use(promised);
2526
const delay = (ms: number) => {
@@ -106,6 +107,30 @@ class InteractionOutputTests {
106107
expect(result).be.true;
107108
}
108109

110+
@test async "should fail returning unexpected value with no validation"() {
111+
const stream = Readable.from(Buffer.from("not boolean", "utf-8"));
112+
const content = new Content("application/json", stream);
113+
114+
const out = new InteractionOutput(content, {}, { type: "boolean" }); // ignoreValidation false by default
115+
try {
116+
const result = await out.value();
117+
expect(result).be.true;
118+
fail("Wrongly allows invalid value");
119+
} catch {
120+
// expected to throw
121+
}
122+
}
123+
124+
@test async "should accept returning unexpected value with no validation"() {
125+
// type boolean should not throw since we set ignoreValidation to true
126+
const stream = Readable.from(Buffer.from("not boolean", "utf-8"));
127+
const content = new Content("application/json", stream);
128+
129+
const out = new InteractionOutput(content, {}, { type: "boolean" }, { ignoreValidation: true });
130+
const result = await out.value();
131+
expect(result).to.eql("not boolean");
132+
}
133+
109134
@test async "should data be used after arrayBuffer"() {
110135
const stream = Readable.from(Buffer.from("true", "utf-8"));
111136
const content = new Content("application/json", stream);

0 commit comments

Comments
 (0)