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!: add initial support for query parameters to exploreDirectory method #94

Merged
merged 3 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions example/directory_discovery.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2024 Contributors to the Eclipse Foundation. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// SPDX-License-Identifier: BSD-3-Clause

// ignore_for_file: avoid_print

import "package:dart_wot/binding_http.dart";
import "package:dart_wot/core.dart";

Future<void> main(List<String> args) async {
final servient = Servient(
clientFactories: [
HttpClientFactory(),
],
);

final wot = await servient.start();
// FIXME(JRKhb): The "things" property currently points to "localhost",
// preventing this example from working
final url = Uri.parse("https://zion.vaimee.com/.well-known/wot");

final thingDiscovery = await wot.exploreDirectory(url);

await for (final thingDescription in thingDiscovery) {
print(thingDescription);
}
}
22 changes: 18 additions & 4 deletions lib/src/core/implementation/wot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,17 @@ class WoT implements scripting_api.WoT {

@override
Future<scripting_api.ThingDiscoveryProcess> exploreDirectory(
Uri url, [
Uri url, {
scripting_api.ThingFilter? filter,
]) async {
int? offset,
int? limit,
scripting_api.DirectoryPayloadFormat? format,
}) async {
// TODO(JKRhb): Add support for the collection format.
if (format == scripting_api.DirectoryPayloadFormat.collection) {
throw ArgumentError('Format "$format" is currently not supported.');
}

final thingDescription = await requestThingDescription(url);

if (!thingDescription.isValidDirectoryThingDescription) {
Expand All @@ -124,8 +132,14 @@ class WoT implements scripting_api.WoT {

final consumedDirectoryThing = await consume(thingDescription);

final interactionOutput =
await consumedDirectoryThing.readProperty("things");
final interactionOutput = await consumedDirectoryThing.readProperty(
"things",
uriVariables: {
if (offset != null) "offset": offset,
if (limit != null) "limit": limit,
if (format != null) "format": format.toString(),
},
);
final rawThingDescriptions = await interactionOutput.value();

if (rawThingDescriptions is! List<Object?>) {
Expand Down
36 changes: 34 additions & 2 deletions lib/src/core/scripting_api/wot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,35 @@ import "discovery/thing_filter.dart";
import "exposed_thing.dart";
import "types.dart";

/// Enumeration for specifying the value of the `format` query parameter when
/// using the `exploreDirectory` discovery method.
///
/// See [section 7.3.2.1.5] of the [WoT Discovery] specification for more
/// information.
///
/// [WoT Discovery]: https://www.w3.org/TR/2023/REC-wot-discovery-20231205
/// [section 7.3.2.1.5]: https://www.w3.org/TR/2023/REC-wot-discovery-20231205/#exploration-directory-api-things-listing
enum DirectoryPayloadFormat {
/// Indicates that an array of Thing Descriptions should be returned.
///
/// This is the default value.
array,

/// Indicates that an collection of Thing Descriptions should be returned.
collection,
;

@override
String toString() {
switch (this) {
case array:
return "array";
case collection:
return "collection";
}
}
}

/// Interface for a [WoT] runtime.
///
/// See WoT Scripting API specification,
Expand All @@ -39,9 +68,12 @@ abstract interface class WoT {
/// [ThingDescription] objects for Thing Descriptions that match an optional
/// [filter] argument of type [ThingFilter].
Future<ThingDiscoveryProcess> exploreDirectory(
Uri url, [
Uri url, {
ThingFilter? filter,
]);
int? offset,
int? limit,
DirectoryPayloadFormat? format,
});

/// Discovers [ThingDescription]s from a given [url] using the specified
/// [method].
Expand Down
76 changes: 74 additions & 2 deletions test/core/discovery_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ final directoryTestUri2 = Uri.parse("$testUriScheme://[::4]/.well-known/wot");
final directoryTestThingsUri2 = Uri.parse("$testUriScheme://[::4]/things");
final directoryTestUri3 = Uri.parse("$testUriScheme://[::5]/.well-known/wot");
final directoryTestThingsUri3 = Uri.parse("$testUriScheme://[::5]/things");
final directoryTestUri4 = Uri.parse("$testUriScheme://[::6]/.well-known/wot");
final directoryTestThingsUri4 = Uri.parse(
"$testUriScheme://[::3]/things?offset=2&limit=3&format=array",
);

const validTestTitle1 = "Test TD 1";
const validTestThingDescription = '''
Expand Down Expand Up @@ -49,6 +53,26 @@ final directoryThingDescription1 = '''
},
"properties": {
"things": {
"uriVariables": {
"offset": {
"title": "Number of TDs to skip before the page",
"type": "number",
"default": 0
},
"limit": {
"title": "Number of TDs in a page",
"type": "number"
},
"format": {
"title": "Payload format",
"type": "string",
"enum": [
"array",
"collection"
],
"default": "array"
}
},
"forms": [
{
"href": "$directoryTestThingsUri1"
Expand Down Expand Up @@ -128,8 +152,9 @@ class _MockedProtocolClient implements ProtocolClient {
}

@override
Future<Content> readResource(Form form) async {
final href = form.href;
Future<Content> readResource(AugmentedForm form) async {
final href = form.resolvedHref;

if (href == directoryTestThingsUri1) {
return "[$validTestThingDescription]".toContent("application/td+json");
}
Expand All @@ -142,6 +167,10 @@ class _MockedProtocolClient implements ProtocolClient {
return invalidTestThingDescription2.toContent("application/td+json");
}

if (href == directoryTestThingsUri4) {
return "[$validTestThingDescription]".toContent("application/ld+json");
}

throw StateError("Encountered an unknown URI $href.");
}

Expand All @@ -167,6 +196,10 @@ class _MockedProtocolClient implements ProtocolClient {
return directoryThingDescription3.toDiscoveryContent(url);
}

if (url == directoryTestUri4) {
return directoryThingDescription1.toDiscoveryContent(url);
}

throw StateError("Encountered invalid URL.");
}

Expand Down Expand Up @@ -377,5 +410,44 @@ void main() {
await thingDiscoveryProcess.stop();
expect(thingDiscoveryProcess.done, true);
});

test("should support the experimental query parameters API", () async {
final servient = Servient(
clientFactories: [
_MockedProtocolClientFactory(),
],
);

final wot = await servient.start();
final thingDiscoveryProcess = await wot.exploreDirectory(
directoryTestUri4,
offset: 2,
limit: 3,
format: DirectoryPayloadFormat.array,
);

var counter = 0;
await for (final thingDescription in thingDiscoveryProcess) {
counter++;
expect(thingDescription.title, validTestTitle1);
}
expect(counter, 1);
expect(thingDiscoveryProcess.done, true);
});

test(
'should currently not support the "collection" format when using the '
"experimental query parameters API", () async {
final servient = Servient();
final wot = await servient.start();

expect(
() async => await wot.exploreDirectory(
directoryTestUri4,
format: DirectoryPayloadFormat.collection,
),
throwsArgumentError,
);
});
});
}