Skip to content

Commit

Permalink
fix(core/interaction-output): allign value function implementation wi…
Browse files Browse the repository at this point in the history
…th 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
  • Loading branch information
relu91 committed Jan 16, 2024
1 parent 6901b5a commit 9a78440
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 41 deletions.
2 changes: 1 addition & 1 deletion packages/binding-http/test/http-server-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class HttpServerTest {
let test: DataSchemaValue;
testThing.setPropertyReadHandler("test", (_) => Promise.resolve(test));
testThing.setPropertyWriteHandler("test", async (value) => {
test = await value.value();
test = Buffer.from(await value.arrayBuffer()).toString("utf-8");
});

testThing.setActionHandler("try", async (input: WoT.InteractionOutput) => {
Expand Down
23 changes: 20 additions & 3 deletions packages/binding-opcua/test/full-opcua-thing-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const thingDescription: WoT.ThingDescription = {
observable: true,
readOnly: true,
unit: "°C",
type: "number",
"opcua:nodeId": { root: "i=84", path: "/Objects/1:MySensor/2:ParameterSet/1:Temperature" },
// Don't specifu type here as it could be multi form: type: [ "object", "number" ],
forms: [
Expand Down Expand Up @@ -111,6 +112,7 @@ const thingDescription: WoT.ThingDescription = {
description: "the temperature set point",
observable: true,
unit: "°C",
type: "number",
// dont't
forms: [
{
Expand Down Expand Up @@ -358,10 +360,25 @@ describe("Full OPCUA Thing Test", () => {

return { thing, servient };
}
async function doTest(thing: WoT.ConsumedThing, propertyName: string, localOptions: InteractionOptions) {
async function doTest(
thing: WoT.ConsumedThing,
propertyName: string,
localOptions: InteractionOptions,
forceParsing = false
) {
debug("------------------------------------------------------");
try {
const content = await thing.readProperty(propertyName, localOptions);
if (forceParsing) {
// In opcua binding it is possible to return a special response that contains
// reacher details than the bare value. However this make the returned value
// not complaint with its data schema. Therefore we have to fallback to
// custom parsing.
const raw = await content.arrayBuffer();
const json = JSON.parse(Buffer.from(raw).toString("utf-8"));
debug(json?.toString());
return json;
}
const json = await content.value();
debug(json?.toString());
return json;
Expand Down Expand Up @@ -395,13 +412,13 @@ describe("Full OPCUA Thing Test", () => {
const json1 = await doTest(thing, propertyName, { formIndex: 1 });
expect(json1).to.eql(25);

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

expect(thingDescription.properties?.temperature.forms[3].contentType).eql(
"application/opcua+json;type=DataValue"
);
const json3 = await doTest(thing, propertyName, { formIndex: 3 });
const json3 = await doTest(thing, propertyName, { formIndex: 3 }, true);
debug(json3?.toString());
expect((json3 as Record<string, unknown>).Value).to.eql({ Type: 11, Body: 25 });
} finally {
Expand Down
54 changes: 29 additions & 25 deletions packages/core/src/interaction-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import * as util from "util";
import * as WoT from "wot-typescript-definitions";
import { ContentSerdes } from "./content-serdes";
import { ProtocolHelpers } from "./core";
import { DataSchemaError, NotSupportedError } from "./errors";
import { DataSchemaError, NotReadableError, NotSupportedError } from "./errors";
import { Content } from "./content";
import Ajv from "ajv";
import { createLoggers } from "./logger";
Expand All @@ -33,7 +33,7 @@ const ajv = new Ajv({ strict: false });

export class InteractionOutput implements WoT.InteractionOutput {
private content: Content;
private parsedValue: unknown;
#value: unknown;
private buffer?: ArrayBuffer;
private _stream?: ReadableStream;
dataUsed: boolean;
Expand Down Expand Up @@ -79,43 +79,47 @@ export class InteractionOutput implements WoT.InteractionOutput {

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

if (this.dataUsed) {
throw new Error("Can't read the stream once it has been already used");
throw new NotReadableError("Can't read the stream once it has been already used");
}

if (this.form == null) {
throw new NotReadableError("No form defined");
}

Check warning on line 92 in packages/core/src/interaction-output.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/interaction-output.ts#L91-L92

Added lines #L91 - L92 were not covered by tests

if (this.schema == null || this.schema.type == null) {
throw new NotReadableError("No schema defined");

Check warning on line 95 in packages/core/src/interaction-output.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/interaction-output.ts#L95

Added line #L95 was not covered by tests
}

// is content type valid?
if (!this.form || !ContentSerdes.get().isSupported(this.content.type)) {
const message = !this.form ? "Missing form" : `Content type ${this.content.type} not supported`;
if (!ContentSerdes.get().isSupported(this.content.type)) {
const message = `Content type ${this.content.type} not supported`;

Check warning on line 100 in packages/core/src/interaction-output.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/interaction-output.ts#L100

Added line #L100 was not covered by tests
throw new NotSupportedError(message);
}

// read fully the stream
const data = await this.content.toBuffer();
const bytes = await this.content.toBuffer();
this.dataUsed = true;
this.buffer = data;
this.buffer = bytes;

// call the contentToValue
// TODO: should be fixed contentToValue MUST define schema as nullable
const value = ContentSerdes.get().contentToValue({ type: this.content.type, body: data }, this.schema ?? {});

// any data (schema)?
if (this.schema) {
// validate the schema
const validate = ajv.compile<T>(this.schema);

if (!validate(value)) {
debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`);
debug(`value: ${value}`);
debug(`Errror: ${validate.errors}`);
throw new DataSchemaError("Invalid value according to DataSchema", value as WoT.DataSchemaValue);
}
const json = ContentSerdes.get().contentToValue({ type: this.content.type, body: bytes }, this.schema);

// validate the schema
const validate = ajv.compile<T>(this.schema);

if (!validate(json)) {
debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`);
debug(`value: ${json}`);
debug(`Errror: ${validate.errors}`);
throw new DataSchemaError("Invalid value according to DataSchema", json as WoT.DataSchemaValue);
}

this.parsedValue = value;
return this.parsedValue as T;
this.#value = json;
return this.#value as T;
}
}
22 changes: 10 additions & 12 deletions packages/core/src/wot-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,34 @@ import Helpers from "./helpers";
import { createLoggers } from "./logger";
import ContentManager from "./content-serdes";
import { getLastValidationErrors, isThingDescription } from "./validation";
import { inspect } from "util";

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

class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess {
constructor(rawThingDescriptions: WoT.DataSchemaValue, filter?: WoT.ThingFilter) {
constructor(private directory: ConsumedThing, public filter?: WoT.ThingFilter) {
this.filter = filter;
this.done = false;
this.rawThingDescriptions = rawThingDescriptions;
}

rawThingDescriptions: WoT.DataSchemaValue;

filter?: WoT.ThingFilter | undefined;
done: boolean;
error?: Error | undefined;
async stop(): Promise<void> {
this.done = true;
}

async *[Symbol.asyncIterator](): AsyncIterator<WoT.ThingDescription> {
if (!(this.rawThingDescriptions instanceof Array)) {
this.error = new Error("Encountered an invalid output value.");
let rawThingDescriptions: WoT.ThingDescription[];
try {
const thingsPropertyOutput = await this.directory.readProperty("things");
rawThingDescriptions = (await thingsPropertyOutput.value()) as WoT.ThingDescription[];
} catch (error) {
this.error = error instanceof Error ? error : new Error(inspect(error));
this.done = true;
return;
}

for (const outputValue of this.rawThingDescriptions) {
for (const outputValue of rawThingDescriptions) {
if (this.done) {
return;
}
Expand Down Expand Up @@ -81,10 +82,7 @@ export default class WoTImpl {
const directoyThingDescription = await this.requestThingDescription(url);
const consumedDirectoy = await this.consume(directoyThingDescription);

const thingsPropertyOutput = await consumedDirectoy.readProperty("things");
const rawThingDescriptions = await thingsPropertyOutput.value();

return new ThingDiscoveryProcess(rawThingDescriptions, filter);
return new ThingDiscoveryProcess(consumedDirectoy, filter);
}

/** @inheritDoc */
Expand Down
1 change: 1 addition & 0 deletions packages/core/test/DiscoveryTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function createDirectoryTestTd(title: string, thingsPropertyHref: string) {
},
properties: {
things: {
type: "array",
forms: [
{
href: thingsPropertyHref,
Expand Down

0 comments on commit 9a78440

Please sign in to comment.