diff --git a/example/ndn/client_example.dart b/example/ndn/client_example.dart new file mode 100644 index 00000000..e22464a8 --- /dev/null +++ b/example/ndn/client_example.dart @@ -0,0 +1,52 @@ +// 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_ndn.dart"; +import "package:dart_wot/core.dart"; +import "package:dart_wot/src/binding_ndn/ndn_config.dart"; + +Future main() async { + final faceUri = Uri.parse("tcp4://localhost:6363"); + final ndnConfig = NdnConfig( + faceUri: faceUri, + ); + + final servient = Servient( + clientFactories: [NdnClientFactory(ndnConfig: ndnConfig)], + ); + + final wot = await servient.start(); + + final thingDescription = { + "@context": "https://www.w3.org/2022/wot/td/v1.1", + "title": "NDN Thing", + "id": "urn:test", + "securityDefinitions": { + "nosec_sc": {"scheme": "nosec"}, + }, + "security": "nosec_sc", + "properties": { + "ping": { + "forms": [ + { + "contentType": "text/plain", + "href": "ndn:///ndn/ping/9001", + }, + ], + }, + }, + }.toThingDescription(); + + final consumedThing = await wot.consume(thingDescription); + + final result = await consumedThing.readProperty("ping"); + + print(await result.value()); + + await servient.shutdown(); +} diff --git a/lib/binding_ndn.dart b/lib/binding_ndn.dart new file mode 100644 index 00000000..dc52b812 --- /dev/null +++ b/lib/binding_ndn.dart @@ -0,0 +1,13 @@ +// 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 + +/// Protocol binding for Named Data Networking (NDN). Follows +/// an experimental [NDN Protocol Binding specification]. +/// +/// [NDN Protocol Binding specification]: https://github.com/JKRhb/wot-binding-templates/tree/ndn-binding +library binding_ndn; + +export "src/binding_ndn/ndn_consumer_factory.dart"; diff --git a/lib/src/binding_coap/coap_client.dart b/lib/src/binding_coap/coap_client.dart index 4d5e7ba5..befdb7e9 100644 --- a/lib/src/binding_coap/coap_client.dart +++ b/lib/src/binding_coap/coap_client.dart @@ -438,11 +438,6 @@ final class CoapClient implements ProtocolClient { return CoapSubscription(coapClient, null, complete); } - @override - Future start() async { - // Do nothing - } - @override Future stop() async {} diff --git a/lib/src/binding_coap/coap_client_factory.dart b/lib/src/binding_coap/coap_client_factory.dart index 74e1d4d2..bd40d701 100644 --- a/lib/src/binding_coap/coap_client_factory.dart +++ b/lib/src/binding_coap/coap_client_factory.dart @@ -36,7 +36,7 @@ final class CoapClientFactory implements ProtocolClientFactory { } @override - ProtocolClient createClient() => CoapClient( + Future createClient() async => CoapClient( coapConfig: coapConfig, pskCredentialsCallback: _pskCredentialsCallback, aceSecurityCallback: _aceSecurityCallback, diff --git a/lib/src/binding_http/http_client.dart b/lib/src/binding_http/http_client.dart index 0320f6cc..8edcd19b 100644 --- a/lib/src/binding_http/http_client.dart +++ b/lib/src/binding_http/http_client.dart @@ -263,11 +263,6 @@ final class HttpClient implements ProtocolClient { return _contentFromResponse(form, response); } - @override - Future start() async { - // Do nothing - } - @override Future stop() async { _client.close(); diff --git a/lib/src/binding_http/http_client_factory.dart b/lib/src/binding_http/http_client_factory.dart index c35e72a0..bc6d565c 100644 --- a/lib/src/binding_http/http_client_factory.dart +++ b/lib/src/binding_http/http_client_factory.dart @@ -33,7 +33,7 @@ final class HttpClientFactory implements ProtocolClientFactory { } @override - ProtocolClient createClient() => HttpClient( + Future createClient() async => HttpClient( basicCredentialsCallback: _basicCredentialsCallback, bearerCredentialsCallback: _bearerCredentialsCallback, ); diff --git a/lib/src/binding_mqtt/mqtt_client.dart b/lib/src/binding_mqtt/mqtt_client.dart index 62f544ef..0efecbc1 100644 --- a/lib/src/binding_mqtt/mqtt_client.dart +++ b/lib/src/binding_mqtt/mqtt_client.dart @@ -166,11 +166,6 @@ final class MqttClient implements ProtocolClient { ..disconnect(); } - @override - Future start() async { - // Do nothing - } - @override Future stop() async { // Do nothing diff --git a/lib/src/binding_mqtt/mqtt_client_factory.dart b/lib/src/binding_mqtt/mqtt_client_factory.dart index 4ceac789..e6d6a2ef 100644 --- a/lib/src/binding_mqtt/mqtt_client_factory.dart +++ b/lib/src/binding_mqtt/mqtt_client_factory.dart @@ -25,7 +25,7 @@ final class MqttClientFactory implements ProtocolClientFactory { _basicCredentialsCallback; @override - ProtocolClient createClient() => MqttClient( + Future createClient() async => MqttClient( mqttConfig: _mqttConfig, basicCredentialsCallback: _basicCredentialsCallback, ); diff --git a/lib/src/binding_ndn/ndn_config.dart b/lib/src/binding_ndn/ndn_config.dart new file mode 100644 index 00000000..ce899416 --- /dev/null +++ b/lib/src/binding_ndn/ndn_config.dart @@ -0,0 +1,25 @@ +// 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 + +import "package:meta/meta.dart"; + +import "ndn_consumer.dart"; + +@immutable + +/// Configuration class used by [NdnClient]s. +class NdnConfig { + /// + const NdnConfig({ + this.faceUri, + }); + + /// [Uri] of the local NDN Forwarding Daemon (NFD). + /// + /// If `null`, then the default [Uri] will be used, connecting the + /// [NdnClient] to the NFD via a Unix Socket. + final Uri? faceUri; +} diff --git a/lib/src/binding_ndn/ndn_consumer.dart b/lib/src/binding_ndn/ndn_consumer.dart new file mode 100644 index 00000000..6d70db42 --- /dev/null +++ b/lib/src/binding_ndn/ndn_consumer.dart @@ -0,0 +1,92 @@ +// 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 + +import "package:dart_ndn/dart_ndn.dart"; +import "package:meta/meta.dart"; + +import "../../core.dart"; +import "ndn_config.dart"; + +/// A WoT [ProtocolClient] acting as an NDN consumer. +@immutable +class NdnClient implements ProtocolClient { + /// Creates a new [NdnClient] from an NDN [_consumer]. + const NdnClient(this._consumer); + + /// Asynchronously creates a new [NdnClient] from an [NdnConfig]. + static Future create(NdnConfig config) async { + final consumer = await Consumer.create(config.faceUri); + return NdnClient(consumer); + } + + final Consumer _consumer; + + @override + Stream discoverDirectly( + Uri uri, { + bool disableMulticast = false, + }) { + // TODO: implement discoverDirectly + throw UnimplementedError(); + } + + @override + Stream discoverWithCoreLinkFormat(Uri uri) { + // TODO: implement discoverWithCoreLinkFormat + throw UnimplementedError(); + } + + @override + Future invokeResource(AugmentedForm form, Content content) { + // TODO: implement invokeResource + throw UnimplementedError(); + } + + @override + Future readResource(AugmentedForm form) async { + // TODO: Add Name.fromUri + final name = Name.fromString(form.href.path); + + final result = await _consumer.expressInterest(name); + + switch (result) { + case DataReceived(:final data): + final iterable = [data.content ?? []]; + + return Content(form.contentType, Stream.fromIterable(iterable)); + default: + throw Exception("TODO"); + } + } + + @override + Future requestThingDescription(Uri url) { + // TODO: implement requestThingDescription + throw UnimplementedError(); + } + + @override + Future stop() async { + await _consumer.shutdown(); + } + + @override + Future subscribeResource( + AugmentedForm form, { + required void Function(Content content) next, + void Function(Exception error)? error, + required void Function() complete, + }) { + // TODO: implement subscribeResource + throw UnimplementedError(); + } + + @override + Future writeResource(AugmentedForm form, Content content) { + // TODO: implement writeResource + throw UnimplementedError(); + } +} diff --git a/lib/src/binding_ndn/ndn_consumer_factory.dart b/lib/src/binding_ndn/ndn_consumer_factory.dart new file mode 100644 index 00000000..cf2eeee5 --- /dev/null +++ b/lib/src/binding_ndn/ndn_consumer_factory.dart @@ -0,0 +1,46 @@ +// 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 + +import "package:meta/meta.dart"; + +import "../../core.dart"; +import "ndn_config.dart"; +import "ndn_consumer.dart"; + +/// A [ProtocolClientFactory] that produces +@immutable +class NdnClientFactory implements ProtocolClientFactory { + /// Creates a new [ProtocolClientFactory] from an [ndnConfig]. + const NdnClientFactory({ + this.ndnConfig = const NdnConfig(), + }); + + /// The [NdnConfig] acting as the blueprint for creating + final NdnConfig ndnConfig; + + @override + Future createClient() async { + return NdnClient.create(ndnConfig); + } + + @override + bool destroy() { + return true; + } + + @override + bool init() { + return true; + } + + @override + Set get schemes => {"ndn"}; + + @override + bool supportsOperation(OperationType operationType, String? subprotocol) { + return operationType == OperationType.readproperty && subprotocol == null; + } +} diff --git a/lib/src/core/implementation/consumed_thing.dart b/lib/src/core/implementation/consumed_thing.dart index c29204c0..8a9e4b1a 100644 --- a/lib/src/core/implementation/consumed_thing.dart +++ b/lib/src/core/implementation/consumed_thing.dart @@ -35,13 +35,13 @@ class ConsumedThing implements scripting_api.ConsumedThing { /// Determines the id of this [ConsumedThing]. String get identifier => thingDescription.identifier; - (ProtocolClient client, AugmentedForm form) _getClientFor( + Future<(ProtocolClient client, AugmentedForm form)> _getClientFor( List
forms, OperationType operationType, InteractionAffordance interactionAffordance, { required int? formIndex, required Map? uriVariables, - }) { + }) async { final augmentedForms = forms .map( (form) => AugmentedForm.new( @@ -59,7 +59,7 @@ class ConsumedThing implements scripting_api.ConsumedThing { if (formIndex >= 0 && formIndex < forms.length) { foundForm = augmentedForms[formIndex]; final scheme = foundForm.href.scheme; - client = servient.clientFor(scheme); + client = await servient.createClient(scheme); } else { throw ArgumentError( 'ConsumedThing "$title" missing formIndex for ' @@ -86,7 +86,7 @@ class ConsumedThing implements scripting_api.ConsumedThing { orElse: () => throw Exception("No matching form found!"), ); final scheme = foundForm.href.scheme; - client = servient.clientFor(scheme); + client = await servient.createClient(scheme); } return (client, foundForm); @@ -108,7 +108,7 @@ class ConsumedThing implements scripting_api.ConsumedThing { ); } - final (ProtocolClient client, AugmentedForm form) = _getClientFor( + final (ProtocolClient client, AugmentedForm form) = await _getClientFor( property.forms, OperationType.readproperty, property, @@ -117,6 +117,8 @@ class ConsumedThing implements scripting_api.ConsumedThing { ); final content = await client.readResource(form); + // FIXME: Clients should be reusable + await client.stop(); return InteractionOutput(content, servient.contentSerdes, form, property); } @@ -137,7 +139,7 @@ class ConsumedThing implements scripting_api.ConsumedThing { ); } - final (client, form) = _getClientFor( + final (client, form) = await _getClientFor( property.forms, OperationType.writeproperty, property, @@ -173,7 +175,7 @@ class ConsumedThing implements scripting_api.ConsumedThing { ); } - final (client, form) = _getClientFor( + final (client, form) = await _getClientFor( action.forms, OperationType.invokeaction, action, @@ -269,7 +271,7 @@ class ConsumedThing implements scripting_api.ConsumedThing { subscriptions = _subscribedEvents; } - final (client, form) = _getClientFor( + final (client, form) = await _getClientFor( affordance.forms, operationType, affordance, diff --git a/lib/src/core/implementation/protocol_interfaces/protocol_client.dart b/lib/src/core/implementation/protocol_interfaces/protocol_client.dart index 570b9ce4..2828ba5a 100644 --- a/lib/src/core/implementation/protocol_interfaces/protocol_client.dart +++ b/lib/src/core/implementation/protocol_interfaces/protocol_client.dart @@ -10,9 +10,6 @@ import "../content.dart"; /// Base class for a Protocol Client. abstract interface class ProtocolClient { - /// Starts this [ProtocolClient]. - Future start(); - /// Stops this [ProtocolClient]. Future stop(); diff --git a/lib/src/core/implementation/protocol_interfaces/protocol_client_factory.dart b/lib/src/core/implementation/protocol_interfaces/protocol_client_factory.dart index 961c5881..1a21a503 100644 --- a/lib/src/core/implementation/protocol_interfaces/protocol_client_factory.dart +++ b/lib/src/core/implementation/protocol_interfaces/protocol_client_factory.dart @@ -26,7 +26,7 @@ abstract interface class ProtocolClientFactory { /// Creates a new [ProtocolClient] with that supports one or more of the given /// [schemes]. - ProtocolClient createClient(); + Future createClient(); /// Indicates whether this [ProtocolClientFactory] supports a given /// [operationType] and subprotocol. diff --git a/lib/src/core/implementation/servient.dart b/lib/src/core/implementation/servient.dart index cc346e25..eb7e62ee 100644 --- a/lib/src/core/implementation/servient.dart +++ b/lib/src/core/implementation/servient.dart @@ -198,7 +198,7 @@ class Servient { _clientFactories.remove(scheme); /// Returns the [ProtocolClient] associated with a given [scheme]. - ProtocolClient clientFor(String scheme) { + Future createClient(String scheme) async { final clientFactory = _clientFactories[scheme]; if (clientFactory == null) { @@ -232,7 +232,7 @@ class Servient { /// Requests a [ThingDescription] from a [url]. Future requestThingDescription(Uri url) async { - final client = clientFor(url.scheme); + final client = await createClient(url.scheme); final content = await client.requestThingDescription(url); final dataSchemaValue = await contentSerdes.contentToValue(content, null); diff --git a/lib/src/core/implementation/thing_discovery.dart b/lib/src/core/implementation/thing_discovery.dart index 662ef941..862fc948 100644 --- a/lib/src/core/implementation/thing_discovery.dart +++ b/lib/src/core/implementation/thing_discovery.dart @@ -69,7 +69,7 @@ class ThingDiscovery extends Stream } } - ProtocolClient _clientForUriScheme(Uri uri) { + Future _clientForUriScheme(Uri uri) async { final uriScheme = uri.scheme; final existingClient = _clients[uriScheme]; @@ -77,7 +77,7 @@ class ThingDiscovery extends Stream return existingClient; } - final newClient = _servient.clientFor(uriScheme); + final newClient = await _servient.createClient(uriScheme); _clients[uriScheme] = newClient; return newClient; } @@ -106,7 +106,7 @@ class ThingDiscovery extends Stream } Stream _discoverDirectly(Uri uri) async* { - final client = _clientForUriScheme(uri); + final client = await _clientForUriScheme(uri); yield* client .discoverDirectly(uri, disableMulticast: true) @@ -180,7 +180,7 @@ class ThingDiscovery extends Stream ) async* { final Set discoveredUris = {}; final discoveryUri = uri.toLinkFormatDiscoveryUri(resourceType); - final client = _clientForUriScheme(uri); + final client = await _clientForUriScheme(uri); await for (final coreWebLink in client.discoverWithCoreLinkFormat(discoveryUri)) { diff --git a/pubspec.yaml b/pubspec.yaml index 63f47116..6eb9bb96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: coap: ^9.0.0 collection: ^1.17.2 curie: ^0.1.0 + dart_ndn: ^0.0.1 dcaf: ^0.1.0 http: ^1.0.0 http_auth: ^1.0.2 @@ -30,3 +31,7 @@ dependencies: typed_data: ^1.3.2 uri: ^1.0.0 uuid: ^4.2.1 + +dependency_overrides: + dart_ndn: + git: https://github.com/hsel-netsys/dart_ndn.git diff --git a/test/binding_coap/binding_coap_test.dart b/test/binding_coap/binding_coap_test.dart index 2f391aa6..6fca9815 100644 --- a/test/binding_coap/binding_coap_test.dart +++ b/test/binding_coap/binding_coap_test.dart @@ -50,9 +50,7 @@ void main() { expect(defaultClientFactory.coapConfig, null); expect(defaultClientFactory.init(), true); - final coapClient = defaultClientFactory.createClient(); - - await coapClient.start(); + final coapClient = await defaultClientFactory.createClient(); await coapClient.stop(); diff --git a/test/core/discovery_test.dart b/test/core/discovery_test.dart index ba7e851f..eb3ecc1c 100644 --- a/test/core/discovery_test.dart +++ b/test/core/discovery_test.dart @@ -203,11 +203,6 @@ class _MockedProtocolClient implements ProtocolClient { throw StateError("Encountered invalid URL."); } - @override - Future start() async { - // Do nothing - } - @override Future stop() async { // Do nothing @@ -242,7 +237,7 @@ class _MockedProtocolClient implements ProtocolClient { class _MockedProtocolClientFactory implements ProtocolClientFactory { @override - ProtocolClient createClient() { + Future createClient() async { return _MockedProtocolClient(); } diff --git a/test/core/servient_test.dart b/test/core/servient_test.dart index e0e672f6..5cc12d58 100644 --- a/test/core/servient_test.dart +++ b/test/core/servient_test.dart @@ -11,7 +11,7 @@ const testUriScheme = "test"; class MockedProtocolClientFactory implements ProtocolClientFactory { @override - ProtocolClient createClient() { + Future createClient() { throw UnimplementedError("Instantiating a client is not supported yet."); } @@ -69,7 +69,7 @@ void main() { final servient = Servient(); expect( - () => servient.clientFor(testUriScheme), + () => servient.createClient(testUriScheme), throwsA(isA()), ); },