Skip to content

Commit 9a78440

Browse files
committed
fix(core/interaction-output): allign value function implementation with spec
Note that the exploreDirectory method has to be updated to reflect the new check for the presence of DataSchema in the value function. Tests have been updated too. In particular opcua required an ad hoc parsing. fix #1216
1 parent 6901b5a commit 9a78440

5 files changed

Lines changed: 61 additions & 41 deletions

File tree

packages/binding-http/test/http-server-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ class HttpServerTest {
160160
let test: DataSchemaValue;
161161
testThing.setPropertyReadHandler("test", (_) => Promise.resolve(test));
162162
testThing.setPropertyWriteHandler("test", async (value) => {
163-
test = await value.value();
163+
test = Buffer.from(await value.arrayBuffer()).toString("utf-8");
164164
});
165165

166166
testThing.setActionHandler("try", async (input: WoT.InteractionOutput) => {

packages/binding-opcua/test/full-opcua-thing-test.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const thingDescription: WoT.ThingDescription = {
4949
observable: true,
5050
readOnly: true,
5151
unit: "°C",
52+
type: "number",
5253
"opcua:nodeId": { root: "i=84", path: "/Objects/1:MySensor/2:ParameterSet/1:Temperature" },
5354
// Don't specifu type here as it could be multi form: type: [ "object", "number" ],
5455
forms: [
@@ -111,6 +112,7 @@ const thingDescription: WoT.ThingDescription = {
111112
description: "the temperature set point",
112113
observable: true,
113114
unit: "°C",
115+
type: "number",
114116
// dont't
115117
forms: [
116118
{
@@ -358,10 +360,25 @@ describe("Full OPCUA Thing Test", () => {
358360

359361
return { thing, servient };
360362
}
361-
async function doTest(thing: WoT.ConsumedThing, propertyName: string, localOptions: InteractionOptions) {
363+
async function doTest(
364+
thing: WoT.ConsumedThing,
365+
propertyName: string,
366+
localOptions: InteractionOptions,
367+
forceParsing = false
368+
) {
362369
debug("------------------------------------------------------");
363370
try {
364371
const content = await thing.readProperty(propertyName, localOptions);
372+
if (forceParsing) {
373+
// In opcua binding it is possible to return a special response that contains
374+
// reacher details than the bare value. However this make the returned value
375+
// not complaint with its data schema. Therefore we have to fallback to
376+
// custom parsing.
377+
const raw = await content.arrayBuffer();
378+
const json = JSON.parse(Buffer.from(raw).toString("utf-8"));
379+
debug(json?.toString());
380+
return json;
381+
}
365382
const json = await content.value();
366383
debug(json?.toString());
367384
return json;
@@ -395,13 +412,13 @@ describe("Full OPCUA Thing Test", () => {
395412
const json1 = await doTest(thing, propertyName, { formIndex: 1 });
396413
expect(json1).to.eql(25);
397414

398-
const json2 = await doTest(thing, propertyName, { formIndex: 2 });
415+
const json2 = await doTest(thing, propertyName, { formIndex: 2 }, true);
399416
expect(json2).to.eql({ Type: 11, Body: 25 });
400417

401418
expect(thingDescription.properties?.temperature.forms[3].contentType).eql(
402419
"application/opcua+json;type=DataValue"
403420
);
404-
const json3 = await doTest(thing, propertyName, { formIndex: 3 });
421+
const json3 = await doTest(thing, propertyName, { formIndex: 3 }, true);
405422
debug(json3?.toString());
406423
expect((json3 as Record<string, unknown>).Value).to.eql({ Type: 11, Body: 25 });
407424
} finally {

packages/core/src/interaction-output.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as util from "util";
1616
import * as WoT from "wot-typescript-definitions";
1717
import { ContentSerdes } from "./content-serdes";
1818
import { ProtocolHelpers } from "./core";
19-
import { DataSchemaError, NotSupportedError } from "./errors";
19+
import { DataSchemaError, NotReadableError, NotSupportedError } from "./errors";
2020
import { Content } from "./content";
2121
import Ajv from "ajv";
2222
import { createLoggers } from "./logger";
@@ -33,7 +33,7 @@ const ajv = new Ajv({ strict: false });
3333

3434
export class InteractionOutput implements WoT.InteractionOutput {
3535
private content: Content;
36-
private parsedValue: unknown;
36+
#value: unknown;
3737
private buffer?: ArrayBuffer;
3838
private _stream?: ReadableStream;
3939
dataUsed: boolean;
@@ -79,43 +79,47 @@ export class InteractionOutput implements WoT.InteractionOutput {
7979

8080
async value<T extends WoT.DataSchemaValue>(): Promise<T> {
8181
// the value has been already read?
82-
if (this.parsedValue !== undefined) {
83-
return this.parsedValue as T;
82+
if (this.#value !== undefined) {
83+
return this.#value as T;
8484
}
8585

8686
if (this.dataUsed) {
87-
throw new Error("Can't read the stream once it has been already used");
87+
throw new NotReadableError("Can't read the stream once it has been already used");
88+
}
89+
90+
if (this.form == null) {
91+
throw new NotReadableError("No form defined");
92+
}
93+
94+
if (this.schema == null || this.schema.type == null) {
95+
throw new NotReadableError("No schema defined");
8896
}
8997

9098
// is content type valid?
91-
if (!this.form || !ContentSerdes.get().isSupported(this.content.type)) {
92-
const message = !this.form ? "Missing form" : `Content type ${this.content.type} not supported`;
99+
if (!ContentSerdes.get().isSupported(this.content.type)) {
100+
const message = `Content type ${this.content.type} not supported`;
93101
throw new NotSupportedError(message);
94102
}
95103

96104
// read fully the stream
97-
const data = await this.content.toBuffer();
105+
const bytes = await this.content.toBuffer();
98106
this.dataUsed = true;
99-
this.buffer = data;
107+
this.buffer = bytes;
100108

101109
// call the contentToValue
102-
// TODO: should be fixed contentToValue MUST define schema as nullable
103-
const value = ContentSerdes.get().contentToValue({ type: this.content.type, body: data }, this.schema ?? {});
104-
105-
// any data (schema)?
106-
if (this.schema) {
107-
// validate the schema
108-
const validate = ajv.compile<T>(this.schema);
109-
110-
if (!validate(value)) {
111-
debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`);
112-
debug(`value: ${value}`);
113-
debug(`Errror: ${validate.errors}`);
114-
throw new DataSchemaError("Invalid value according to DataSchema", value as WoT.DataSchemaValue);
115-
}
110+
const json = ContentSerdes.get().contentToValue({ type: this.content.type, body: bytes }, this.schema);
111+
112+
// validate the schema
113+
const validate = ajv.compile<T>(this.schema);
114+
115+
if (!validate(json)) {
116+
debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`);
117+
debug(`value: ${json}`);
118+
debug(`Errror: ${validate.errors}`);
119+
throw new DataSchemaError("Invalid value according to DataSchema", json as WoT.DataSchemaValue);
116120
}
117121

118-
this.parsedValue = value;
119-
return this.parsedValue as T;
122+
this.#value = json;
123+
return this.#value as T;
120124
}
121125
}

packages/core/src/wot-impl.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,34 @@ import Helpers from "./helpers";
2222
import { createLoggers } from "./logger";
2323
import ContentManager from "./content-serdes";
2424
import { getLastValidationErrors, isThingDescription } from "./validation";
25+
import { inspect } from "util";
2526

2627
const { debug } = createLoggers("core", "wot-impl");
2728

2829
class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess {
29-
constructor(rawThingDescriptions: WoT.DataSchemaValue, filter?: WoT.ThingFilter) {
30+
constructor(private directory: ConsumedThing, public filter?: WoT.ThingFilter) {
3031
this.filter = filter;
3132
this.done = false;
32-
this.rawThingDescriptions = rawThingDescriptions;
3333
}
3434

35-
rawThingDescriptions: WoT.DataSchemaValue;
36-
37-
filter?: WoT.ThingFilter | undefined;
3835
done: boolean;
3936
error?: Error | undefined;
4037
async stop(): Promise<void> {
4138
this.done = true;
4239
}
4340

4441
async *[Symbol.asyncIterator](): AsyncIterator<WoT.ThingDescription> {
45-
if (!(this.rawThingDescriptions instanceof Array)) {
46-
this.error = new Error("Encountered an invalid output value.");
42+
let rawThingDescriptions: WoT.ThingDescription[];
43+
try {
44+
const thingsPropertyOutput = await this.directory.readProperty("things");
45+
rawThingDescriptions = (await thingsPropertyOutput.value()) as WoT.ThingDescription[];
46+
} catch (error) {
47+
this.error = error instanceof Error ? error : new Error(inspect(error));
4748
this.done = true;
4849
return;
4950
}
5051

51-
for (const outputValue of this.rawThingDescriptions) {
52+
for (const outputValue of rawThingDescriptions) {
5253
if (this.done) {
5354
return;
5455
}
@@ -81,10 +82,7 @@ export default class WoTImpl {
8182
const directoyThingDescription = await this.requestThingDescription(url);
8283
const consumedDirectoy = await this.consume(directoyThingDescription);
8384

84-
const thingsPropertyOutput = await consumedDirectoy.readProperty("things");
85-
const rawThingDescriptions = await thingsPropertyOutput.value();
86-
87-
return new ThingDiscoveryProcess(rawThingDescriptions, filter);
85+
return new ThingDiscoveryProcess(consumedDirectoy, filter);
8886
}
8987

9088
/** @inheritDoc */

packages/core/test/DiscoveryTest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function createDirectoryTestTd(title: string, thingsPropertyHref: string) {
3636
},
3737
properties: {
3838
things: {
39+
type: "array",
3940
forms: [
4041
{
4142
href: thingsPropertyHref,

0 commit comments

Comments
 (0)