Skip to content

Commit

Permalink
Merge pull request #1279 from danielpeintner/issue-1278
Browse files Browse the repository at this point in the history
refactor: handle "non"-validating output for async actions
  • Loading branch information
relu91 authored Jun 7, 2024
2 parents 5c17b56 + f14ae6b commit 3cbd73c
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 9 deletions.
14 changes: 8 additions & 6 deletions packages/core/src/consumed-thing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -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";
Expand All @@ -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<WoT.PropertyReadMap> {
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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}`);
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/interaction-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}

Expand Down Expand Up @@ -122,14 +124,14 @@ export class InteractionOutput implements WoT.InteractionOutput {
// validate the schema
const validate = ajv.compile<T>(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}`);
throw new DataSchemaError("Invalid value according to DataSchema", json as WoT.DataSchemaValue);
}

this.#value = json;
return json;
return json as T;
}
}
34 changes: 34 additions & 0 deletions packages/core/test/ClientTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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")));
Expand Down
25 changes: 25 additions & 0 deletions packages/core/test/InteractionOutputTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 3cbd73c

Please sign in to comment.