Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): implement exploreDirectory method #1186

Merged
merged 21 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bfcf85c
feat(content-serdes): add application/ld+json to supported Content-Types
JKRhb Dec 7, 2023
77b916f
feat(core): implement `exploreDirectory` method
JKRhb Dec 7, 2023
e9c4881
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 14, 2023
4a50219
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
455af97
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
9faa9d2
test: add test for `exploreDirectory` method
JKRhb Dec 15, 2023
db19eae
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 15, 2023
7eddb7f
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
764d9fa
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 15, 2023
0514567
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
57affb8
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
951ca10
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
cddd356
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
99e102b
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
6f11544
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
568ebcc
refactor(core): use validation functions for requestThingDescription
JKRhb Dec 21, 2023
5e17c5f
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
a06e024
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
66104cf
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
c8fc93f
fixup! test: add test for `exploreDirectory` method
JKRhb Dec 21, 2023
7cc777f
fixup! feat(core): implement `exploreDirectory` method
JKRhb Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/content-serdes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class ContentSerdes {
this.instance.addCodec(new JsonCodec(), true);
this.instance.addCodec(new JsonCodec("application/senml+json"));
this.instance.addCodec(new JsonCodec("application/td+json"));
this.instance.addCodec(new JsonCodec("application/ld+json"));
// CBOR
this.instance.addCodec(new CborCodec(), true);
// Text
Expand Down
16 changes: 4 additions & 12 deletions packages/core/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,21 @@ import Servient from "./servient";
import * as TD from "@node-wot/td-tools";
import * as TDT from "wot-thing-description-types";
import { ContentSerdes } from "./content-serdes";
import Ajv, { ValidateFunction, ErrorObject } from "ajv";
import { ValidateFunction, ErrorObject } from "ajv";
import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json";
import { DataSchemaValue, ExposedThingInit } from "wot-typescript-definitions";
import { SomeJSONSchema } from "ajv/dist/types/json-schema";
import { ThingInteraction, ThingModelHelpers } from "@node-wot/td-tools";
import { Resolver } from "@node-wot/td-tools/src/resolver-interface";
import { PropertyElement, DataSchema } from "wot-thing-description-types";
import { createLoggers } from "./logger";
import { createWotAjvInstance } from "./validation";

const { debug, error, warn } = createLoggers("core", "helpers");

const tdSchema = TDSchema;
// RegExps take from https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts
const ajv = new Ajv({ strict: false })
.addFormat(
"iri-reference",
/^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i
)
.addFormat("uri", /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/)
.addFormat(
"date-time",
/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/
);
const ajv = createWotAjvInstance();

export default class Helpers implements Resolver {
static tsSchemaValidator = ajv.compile(Helpers.createExposeThingInitSchema(tdSchema)) as ValidateFunction;

Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/********************************************************************************
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
* Document License (2015-05-13) which is available at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/

import TDSchema from "wot-thing-description-types/schema/td-json-schema-validation.json";
import Ajv from "ajv";

export function createWotAjvInstance() {
return (
new Ajv({ strict: false })
// RegExps taken from https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts
.addFormat(
"iri-reference",
/^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i
)
.addFormat("uri", /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/)
.addFormat(
"date-time",
/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/
)
);
}

const ajv = createWotAjvInstance();
const validateTd = ajv.compile<WoT.ThingDescription>(TDSchema);

export function isThingDescription(input: unknown): input is WoT.ThingDescription {
return validateTd(input);
}
65 changes: 64 additions & 1 deletion packages/core/src/wot-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,64 @@
import { createLoggers } from "./logger";
import ContentManager from "./content-serdes";
import { ErrorObject } from "ajv";
import { ThingDescription } from "wot-thing-description-types";
import { isThingDescription } from "./validation";

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

// @ts-expect-error Typescript currently encounters an error here that *should* be a false positve
class ExploreDirectoryDatasource implements UnderlyingDefaultSource<WoT.ThingDescription> {
constructor(directoryOutput: WoT.DataSchemaValue) {
this.directoryOutput = directoryOutput;
}

directoryOutput: WoT.DataSchemaValue;

start(controller: ReadableStreamDefaultController<WoT.ThingDescription>) {
if (!(this.directoryOutput instanceof Array)) {
controller.error(new Error("Encountered an invalid output value."));
controller.close();
return;
}

Check warning on line 43 in packages/core/src/wot-impl.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/wot-impl.ts#L40-L43

Added lines #L40 - L43 were not covered by tests

for (const outputValue of this.directoryOutput) {
if (!isThingDescription(outputValue)) {
const validationError = new Error("Validation of Thing Description failed");
controller.error(validationError);
continue;
}

Check warning on line 50 in packages/core/src/wot-impl.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/wot-impl.ts#L47-L50

Added lines #L47 - L50 were not covered by tests

controller.enqueue(outputValue);
}

controller.close();
}
}

class ThingDiscoveryProcess implements WoT.ThingDiscoveryProcess {
constructor(thingDescriptionStream: ReadableStream<ThingDescription>, filter?: WoT.ThingFilter) {
this.filter = filter;
this.done = false;
this.thingDescriptionStream = thingDescriptionStream;
}

thingDescriptionStream: ReadableStream<WoT.ThingDescription>;

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

Check warning on line 74 in packages/core/src/wot-impl.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/wot-impl.ts#L72-L74

Added lines #L72 - L74 were not covered by tests

async *[Symbol.asyncIterator](): AsyncIterator<WoT.ThingDescription> {
// @ts-expect-error Typescript currently encounters an error here that *should* be a false positve
yield* this.thingDescriptionStream;
this.done = true;
}
}

export default class WoTImpl {
private srv: Servient;
constructor(srv: Servient) {
Expand All @@ -38,7 +93,15 @@

/** @inheritDoc */
async exploreDirectory(url: string, filter?: WoT.ThingFilter): Promise<WoT.ThingDiscoveryProcess> {
throw new Error("not implemented");
const directoyThingDescription = await this.requestThingDescription(url);
const consumedDirectoy = await this.consume(directoyThingDescription);

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

const thingDescriptionStream = new ReadableStream(thingDescriptionDataSource);
return new ThingDiscoveryProcess(thingDescriptionStream, filter);
}

/** @inheritDoc */
Expand Down
135 changes: 135 additions & 0 deletions packages/core/test/DiscoveryTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/********************************************************************************
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
* Document License (2015-05-13) which is available at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/

import { Form, SecurityScheme } from "@node-wot/td-tools";
import { Subscription } from "rxjs/Subscription";
import { Content } from "../src/content";
import { createLoggers } from "../src/logger";
import { ProtocolClient, ProtocolClientFactory } from "../src/protocol-interfaces";
import Servient from "../src/servient";
import { Readable } from "stream";
import { expect } from "chai";

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

const directoryTdUrl = "test://localhost/.well-known/wot";
const directoryThingsUrl = "test://localhost/things";

const directoryThingDescription = {
"@context": "https://www.w3.org/2022/wot/td/v1.1",
title: "Directory Test TD",
security: "nosec_sc",
securityDefinitions: {
nosec_sc: {
scheme: "nosec",
},
},
properties: {
things: {
forms: [
{
href: directoryThingsUrl,
},
],
},
},
};

class TestProtocolClient implements ProtocolClient {
async readResource(form: Form): Promise<Content> {
if (form.href === directoryThingsUrl) {
const buffer = Buffer.from(JSON.stringify([directoryThingDescription]));
const content = new Content("application/ld+json", Readable.from(buffer));
return content;
}

throw new Error("Invalid URL");
}

writeResource(form: Form, content: Content): Promise<void> {
throw new Error("Method not implemented.");
}

invokeResource(form: Form, content?: Content | undefined): Promise<Content> {
throw new Error("Method not implemented.");
}

unlinkResource(form: Form): Promise<void> {
throw new Error("Method not implemented.");
}

subscribeResource(
form: Form,
next: (content: Content) => void,
error?: ((error: Error) => void) | undefined,
complete?: (() => void) | undefined
): Promise<Subscription> {
throw new Error("Method not implemented.");
}

async requestThingDescription(uri: string): Promise<Content> {
if (uri === directoryTdUrl) {
debug(`Found corrent URL ${directoryTdUrl} to fetch directory TD`);
const buffer = Buffer.from(JSON.stringify(directoryThingDescription));
const content = new Content("application/td+json", Readable.from(buffer));
return content;
}

throw Error("Invalid URL");
}

async start(): Promise<void> {
// Do nothing
}

async stop(): Promise<void> {
// Do nothing
}

setSecurity(metadata: SecurityScheme[], credentials?: unknown): boolean {
return true;
}
}

class TestProtocolClientFactory implements ProtocolClientFactory {
public scheme = "test";

getClient(): ProtocolClient {
return new TestProtocolClient();
}

init(): boolean {
return true;
}

destroy(): boolean {
return true;
}
}

describe("Discovery Tests", () => {
it("should be possible to use the exploreDirectory method", async () => {
const servient = new Servient();
servient.addClientFactory(new TestProtocolClientFactory());

const WoT = await servient.start();

const discoveryProcess = await WoT.exploreDirectory(directoryTdUrl);

for await (const thingDescription of discoveryProcess) {
expect(thingDescription.title === "Directory Test TD");
}
});
});
Loading