Skip to content

Commit

Permalink
Merge pull request #1251 from danielpeintner/issue-1250
Browse files Browse the repository at this point in the history
refactor: handle also enhanced contentTypes
  • Loading branch information
relu91 authored Apr 4, 2024
2 parents ff32218 + df20650 commit 8d2fae9
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 23 deletions.
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"main": "dist/core.js",
"types": "dist/core.d.ts",
"devDependencies": {
"@types/content-type": "^1.1.8",
"@types/debug": "^4.1.7",
"@types/uritemplate": "^0.3.4",
"@types/uuid": "^8.3.1"
Expand All @@ -24,6 +25,7 @@
"@petamoriken/float16": "^3.1.1",
"ajv": "^8.11.0",
"cbor": "^8.1.0",
"content-type": "^1.0.5",
"debug": "^4.3.4",
"uritemplate": "0.3.4",
"uuid": "^7.0.3",
Expand Down
60 changes: 39 additions & 21 deletions packages/core/src/consumed-thing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import Servient from "./servient";
import Helpers from "./helpers";

import { ProtocolClient } from "./protocol-interfaces";
import { Content } from "./content";
import ContentType from "content-type";

import ContentManager from "./content-serdes";

Expand Down Expand Up @@ -555,7 +557,33 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
form = this.handleUriVariables(tp, form, options);

const content = await client.readResource(form);
return new InteractionOutput(content, form, tp);
try {
return this.handleInteractionOutput(content, form, tp);
} 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}`);
}
}

private handleInteractionOutput(
content: Content,
form: TD.Form,
outputDataSchema: WoT.DataSchema | undefined
): InteractionOutput {
// infer media type from form if not in response metadata
content.type ??= form.contentType ?? "application/json";

// check if returned media type is the same as expected media type (from TD)
if (form.response != null) {
const parsedMediaTypeContent = ContentType.parse(content.type);
const parsedMediaTypeForm = ContentType.parse(form.response.contentType);
if (parsedMediaTypeContent.type !== parsedMediaTypeForm.type) {
throw new Error(
`Unexpected type '${content.type}' in response. Should be '${form.response.contentType}'`
);
}
}
return new InteractionOutput(content, form, outputDataSchema);
}

async _readProperties(propertyNames: string[]): Promise<WoT.PropertyReadMap> {
Expand Down Expand Up @@ -674,19 +702,11 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
form = this.handleUriVariables(ta, form, options);

const content = await client.invokeResource(form, input);
// infer media type from form if not in response metadata
if (!content.type) content.type = form.contentType ?? "application/json";

// check if returned media type is the same as expected media type (from TD)
if (form.response != null) {
if (content.type !== form.response.contentType) {
throw new Error(`Unexpected type in response`);
}
}
try {
return new InteractionOutput(content, form, ta.output);
} catch {
throw new Error(`Received invalid content from Thing`);
return this.handleInteractionOutput(content, form, ta.output);
} 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 @@ -725,12 +745,11 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
formWithoutURITemplates,
// next
(content) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- tsc get confused when nullables are to listeners lambdas
if (!content.type) content.type = form!.contentType ?? "application/json";
try {
listener(new InteractionOutput(content, form, tp));
listener(this.handleInteractionOutput(content, form, tp));
} catch (e) {
warn(`Error while processing observe event for ${tp.title}`);
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
warn(`Error while processing observe property for ${tp.title}. ${error.message}`);
warn(e);
}
},
Expand Down Expand Up @@ -782,12 +801,11 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
await client.subscribeResource(
formWithoutURITemplates,
(content) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- tsc get confused when nullables are to listeners lambdas
if (!content.type) content.type = form!.contentType ?? "application/json";
try {
listener(new InteractionOutput(content, form, te.data));
listener(this.handleInteractionOutput(content, form, te.data));
} catch (e) {
warn(`Error while processing event for ${te.title}`);
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
warn(`Error while processing event for ${te.title}. ${error.message}`);
warn(e);
}
},
Expand Down
50 changes: 49 additions & 1 deletion packages/core/test/ClientTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { Readable } from "stream";
import { createLoggers, ProtocolHelpers } from "../src/core";
import { ThingDescription } from "wot-typescript-definitions";
import chaiAsPromised from "chai-as-promised";
import { fail } from "assert";

const { debug } = createLoggers("core", "ClientTest");

Expand Down Expand Up @@ -99,7 +100,13 @@ const myThingDesc = {
anAction: {
input: { type: "integer" },
output: { type: "integer" },
forms: [{ href: "testdata://host/athing/actions/anaction", mediaType: "application/json" }],
forms: [
{
href: "testdata://host/athing/actions/anaction",
mediaType: "application/json",
response: { contentType: "application/json" },
},
],
},
},
events: {
Expand Down Expand Up @@ -462,6 +469,47 @@ class WoTClientTest {
expect(value).to.equal(42);
}

@test async "call an action with enhanced contentType"() {
// an action
WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => {
const valueData = await content.toBuffer();
expect(valueData.toString()).to.equal("23");
return new Content("application/json; charset=utf-8", Readable.from(Buffer.from("42")));
});
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");
const result = await thing.invokeAction("anAction", 23);
// eslint-disable-next-line no-unused-expressions
expect(result).not.to.be.null;
const value = await result?.value();
expect(value).to.equal(42);
}

@test async "call an action with wrong contentType based on TD response"() {
// an action
WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => {
const valueData = await content.toBuffer();
expect(valueData.toString()).to.equal("23");
// Note: application/json expected based on TD response
return new Content("text/plain", Readable.from(Buffer.from("42")));
});
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");
try {
await thing.invokeAction("anAction", 23);
fail("Should report unexpected content type");
} catch (e) {
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
expect(error.message).to.contain("type");
}
}

@test async "subscribe to event"() {
WoTClientTest.clientFactory.setTrap(() => {
return new Content("application/json", Readable.from(Buffer.from("triggered")));
Expand Down

0 comments on commit 8d2fae9

Please sign in to comment.