Skip to content

Commit 8d2fae9

Browse files
authored
Merge pull request #1251 from danielpeintner/issue-1250
refactor: handle also enhanced contentTypes
2 parents ff32218 + df20650 commit 8d2fae9

4 files changed

Lines changed: 98 additions & 23 deletions

File tree

package-lock.json

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"main": "dist/core.js",
1616
"types": "dist/core.d.ts",
1717
"devDependencies": {
18+
"@types/content-type": "^1.1.8",
1819
"@types/debug": "^4.1.7",
1920
"@types/uritemplate": "^0.3.4",
2021
"@types/uuid": "^8.3.1"
@@ -24,6 +25,7 @@
2425
"@petamoriken/float16": "^3.1.1",
2526
"ajv": "^8.11.0",
2627
"cbor": "^8.1.0",
28+
"content-type": "^1.0.5",
2729
"debug": "^4.3.4",
2830
"uritemplate": "0.3.4",
2931
"uuid": "^7.0.3",

packages/core/src/consumed-thing.ts

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import Servient from "./servient";
2121
import Helpers from "./helpers";
2222

2323
import { ProtocolClient } from "./protocol-interfaces";
24+
import { Content } from "./content";
25+
import ContentType from "content-type";
2426

2527
import ContentManager from "./content-serdes";
2628

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

557559
const content = await client.readResource(form);
558-
return new InteractionOutput(content, form, tp);
560+
try {
561+
return this.handleInteractionOutput(content, form, tp);
562+
} catch (e) {
563+
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
564+
throw new Error(`Error while processing property for ${tp.title}. ${error.message}`);
565+
}
566+
}
567+
568+
private handleInteractionOutput(
569+
content: Content,
570+
form: TD.Form,
571+
outputDataSchema: WoT.DataSchema | undefined
572+
): InteractionOutput {
573+
// infer media type from form if not in response metadata
574+
content.type ??= form.contentType ?? "application/json";
575+
576+
// check if returned media type is the same as expected media type (from TD)
577+
if (form.response != null) {
578+
const parsedMediaTypeContent = ContentType.parse(content.type);
579+
const parsedMediaTypeForm = ContentType.parse(form.response.contentType);
580+
if (parsedMediaTypeContent.type !== parsedMediaTypeForm.type) {
581+
throw new Error(
582+
`Unexpected type '${content.type}' in response. Should be '${form.response.contentType}'`
583+
);
584+
}
585+
}
586+
return new InteractionOutput(content, form, outputDataSchema);
559587
}
560588

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

676704
const content = await client.invokeResource(form, input);
677-
// infer media type from form if not in response metadata
678-
if (!content.type) content.type = form.contentType ?? "application/json";
679-
680-
// check if returned media type is the same as expected media type (from TD)
681-
if (form.response != null) {
682-
if (content.type !== form.response.contentType) {
683-
throw new Error(`Unexpected type in response`);
684-
}
685-
}
686705
try {
687-
return new InteractionOutput(content, form, ta.output);
688-
} catch {
689-
throw new Error(`Received invalid content from Thing`);
706+
return this.handleInteractionOutput(content, form, ta.output);
707+
} catch (e) {
708+
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
709+
throw new Error(`Error while processing action for ${ta.title}. ${error.message}`);
690710
}
691711
}
692712

@@ -725,12 +745,11 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
725745
formWithoutURITemplates,
726746
// next
727747
(content) => {
728-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- tsc get confused when nullables are to listeners lambdas
729-
if (!content.type) content.type = form!.contentType ?? "application/json";
730748
try {
731-
listener(new InteractionOutput(content, form, tp));
749+
listener(this.handleInteractionOutput(content, form, tp));
732750
} catch (e) {
733-
warn(`Error while processing observe event for ${tp.title}`);
751+
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
752+
warn(`Error while processing observe property for ${tp.title}. ${error.message}`);
734753
warn(e);
735754
}
736755
},
@@ -782,12 +801,11 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing {
782801
await client.subscribeResource(
783802
formWithoutURITemplates,
784803
(content) => {
785-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- tsc get confused when nullables are to listeners lambdas
786-
if (!content.type) content.type = form!.contentType ?? "application/json";
787804
try {
788-
listener(new InteractionOutput(content, form, te.data));
805+
listener(this.handleInteractionOutput(content, form, te.data));
789806
} catch (e) {
790-
warn(`Error while processing event for ${te.title}`);
807+
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
808+
warn(`Error while processing event for ${te.title}. ${error.message}`);
791809
warn(e);
792810
}
793811
},

packages/core/test/ClientTest.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { Readable } from "stream";
3636
import { createLoggers, ProtocolHelpers } from "../src/core";
3737
import { ThingDescription } from "wot-typescript-definitions";
3838
import chaiAsPromised from "chai-as-promised";
39+
import { fail } from "assert";
3940

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

@@ -99,7 +100,13 @@ const myThingDesc = {
99100
anAction: {
100101
input: { type: "integer" },
101102
output: { type: "integer" },
102-
forms: [{ href: "testdata://host/athing/actions/anaction", mediaType: "application/json" }],
103+
forms: [
104+
{
105+
href: "testdata://host/athing/actions/anaction",
106+
mediaType: "application/json",
107+
response: { contentType: "application/json" },
108+
},
109+
],
103110
},
104111
},
105112
events: {
@@ -462,6 +469,47 @@ class WoTClientTest {
462469
expect(value).to.equal(42);
463470
}
464471

472+
@test async "call an action with enhanced contentType"() {
473+
// an action
474+
WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => {
475+
const valueData = await content.toBuffer();
476+
expect(valueData.toString()).to.equal("23");
477+
return new Content("application/json; charset=utf-8", Readable.from(Buffer.from("42")));
478+
});
479+
const td = (await WoTClientTest.WoTHelpers.fetch("td://foo")) as ThingDescription;
480+
const thing = await WoTClientTest.WoT.consume(td);
481+
482+
expect(thing).to.have.property("title").that.equals("aThing");
483+
expect(thing).to.have.property("actions").that.has.property("anAction");
484+
const result = await thing.invokeAction("anAction", 23);
485+
// eslint-disable-next-line no-unused-expressions
486+
expect(result).not.to.be.null;
487+
const value = await result?.value();
488+
expect(value).to.equal(42);
489+
}
490+
491+
@test async "call an action with wrong contentType based on TD response"() {
492+
// an action
493+
WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => {
494+
const valueData = await content.toBuffer();
495+
expect(valueData.toString()).to.equal("23");
496+
// Note: application/json expected based on TD response
497+
return new Content("text/plain", Readable.from(Buffer.from("42")));
498+
});
499+
const td = (await WoTClientTest.WoTHelpers.fetch("td://foo")) as ThingDescription;
500+
const thing = await WoTClientTest.WoT.consume(td);
501+
502+
expect(thing).to.have.property("title").that.equals("aThing");
503+
expect(thing).to.have.property("actions").that.has.property("anAction");
504+
try {
505+
await thing.invokeAction("anAction", 23);
506+
fail("Should report unexpected content type");
507+
} catch (e) {
508+
const error = e instanceof Error ? e : new Error(JSON.stringify(e));
509+
expect(error.message).to.contain("type");
510+
}
511+
}
512+
465513
@test async "subscribe to event"() {
466514
WoTClientTest.clientFactory.setTrap(() => {
467515
return new Content("application/json", Readable.from(Buffer.from("triggered")));

0 commit comments

Comments
 (0)