diff --git a/example/exposed_thing/http_server.dart b/example/exposed_thing/http_server.dart new file mode 100644 index 00000000..4d240c80 --- /dev/null +++ b/example/exposed_thing/http_server.dart @@ -0,0 +1,111 @@ +// 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"; + +String property = "hi :)"; + +void main() async { + final servient = Servient.create( + clientFactories: [HttpClientFactory()], + servers: [HttpServer(HttpConfig(port: 3000))], + ); + + final wot = await servient.start(); + + final exposedThing = await wot.produce({ + "@context": "https://www.w3.org/2022/wot/td/v1.1", + "title": "My Lamp Thing", + "id": "test", + "properties": { + "status": { + "type": "string", + "forms": [ + { + "href": "/status", + } + ], + }, + }, + "actions": { + "toggle": { + "input": { + "type": "boolean", + }, + "output": { + "type": "null", + }, + "forms": [ + { + "href": "/toggle", + } + ], + }, + }, + }); + + exposedThing + ..setPropertyReadHandler("status", ({ + data, + formIndex, + uriVariables, + }) async { + return InteractionInput.fromString(property); + }) + ..setPropertyWriteHandler("status", ( + interactionOutput, { + data, + formIndex, + uriVariables, + }) async { + final value = await interactionOutput.value(); + + if (value is String) { + property = value; + return; + } + + throw const FormatException(); + }) + ..setActionHandler("toggle", ( + actionInput, { + data, + formIndex, + uriVariables, + }) async { + print(await actionInput.value()); + + return InteractionInput.fromNull(); + }); + + final thingDescription = await wot + .requestThingDescription(Uri.parse("http://localhost:3000/test")); + print(thingDescription.toJson()); + final consumedThing = await wot.consume(thingDescription); + + var value = await (await consumedThing.readProperty("status")).value(); + print(value); + + await consumedThing.writeProperty( + "status", + DataSchemaValueInput(DataSchemaValue.fromString("bye")), + ); + + value = await (await consumedThing.readProperty("status")).value(); + print(value); + + final actionOutput = await consumedThing.invokeAction( + "toggle", + input: InteractionInput.fromBoolean(true), + ); + + print(await actionOutput.value()); + + await servient.shutdown(); +} diff --git a/lib/src/binding_coap/coap_server.dart b/lib/src/binding_coap/coap_server.dart index f1bd2343..a857bba3 100644 --- a/lib/src/binding_coap/coap_server.dart +++ b/lib/src/binding_coap/coap_server.dart @@ -26,13 +26,13 @@ final class CoapServer implements ProtocolServer { final int? preferredBlockSize; @override - Future expose(ExposedThing thing) { + Future expose(ExposableThing thing) { // TODO(JKRhb): implement expose throw UnimplementedError(); } @override - Future start([ServerSecurityCallback? serverSecurityCallback]) { + Future start(Servient servient) { // TODO(JKRhb): implement start throw UnimplementedError(); } @@ -42,4 +42,10 @@ final class CoapServer implements ProtocolServer { // TODO(JKRhb): implement stop throw UnimplementedError(); } + + @override + Future destroyThing(ExposableThing thing) { + // TODO: implement destroyThing + throw UnimplementedError(); + } } diff --git a/lib/src/binding_http/http_extensions.dart b/lib/src/binding_http/http_extensions.dart new file mode 100644 index 00000000..af80746f --- /dev/null +++ b/lib/src/binding_http/http_extensions.dart @@ -0,0 +1,41 @@ +// 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 "../../core.dart"; + +/// Extension for determining the HTTP method that corresponds with an +/// [OperationType]. +extension HttpMethodExtension on OperationType { + /// Returns the default HTTP method as defined in the [HTTP binding template] + /// specification. + /// + /// If the [OperationType] value has no default method defined, an + /// [ArgumentError] will be thrown. + /// + /// [HTTP binding template]: https://w3c.github.io/wot-binding-templates/bindings/protocols/http/#http-default-vocabulary-terms + String get defaultHttpMethod { + switch (this) { + case OperationType.readproperty: + return "GET"; + case OperationType.writeproperty: + return "PUT"; + case OperationType.invokeaction: + return "POST"; + case OperationType.readallproperties: + return "GET"; + case OperationType.writeallproperties: + return "PUT"; + case OperationType.readmultipleproperties: + return "GET"; + case OperationType.writemultipleproperties: + return "PUT"; + default: + throw ArgumentError( + "OperationType $this has no default HTTP method defined.", + ); + } + } +} diff --git a/lib/src/binding_http/http_server.dart b/lib/src/binding_http/http_server.dart index a0f5c831..1531cd07 100644 --- a/lib/src/binding_http/http_server.dart +++ b/lib/src/binding_http/http_server.dart @@ -4,9 +4,16 @@ // // SPDX-License-Identifier: BSD-3-Clause -import "../../core.dart"; +import "dart:io" as io; + +import "package:shelf/shelf.dart"; +import "package:shelf/shelf_io.dart" as shelf_io; +import "package:shelf_router/shelf_router.dart"; + +import "../../core.dart" hide ExposedThing; import "http_config.dart"; +import "http_extensions.dart"; /// A [ProtocolServer] for the Hypertext Transfer Protocol (HTTP). final class HttpServer implements ProtocolServer { @@ -22,6 +29,17 @@ final class HttpServer implements ProtocolServer { @override final int port; + // FIXME + final Object _bindAddress = io.InternetAddress.loopbackIPv4; + + io.HttpServer? _server; + + final _things = {}; + + late final Servient _servient; + + final Map _routes = {}; + static int _portFromConfig(HttpConfig? httpConfig) { final secure = httpConfig?.secure ?? false; @@ -29,20 +47,208 @@ final class HttpServer implements ProtocolServer { } @override - Future expose(ExposedThing thing) { - // TODO(JKRhb): implement expose - throw UnimplementedError(); + Future expose(ExposableThing thing) async { + final thingDescription = thing.thingDescription; + final thingId = thingDescription.id; + + if (thingId == null) { + throw ArgumentError("Missing id field in thingDescription."); + } + + _things[thingId] = thing; + + final router = Router() + ..get("/$thingId", (request) { + const defaultContentType = "application/td+json"; + return Response( + 200, + body: _servient.contentSerdes + .valueToContent( + DataSchemaValue.tryParse( + thingDescription.toJson(), + ), + null, + defaultContentType, + ) + .body, + headers: { + "Content-Type": defaultContentType, + }, + ); + }); + + final affordances = >[]; + + for (final affordanceMap in [ + thingDescription.actions, + thingDescription.properties, + thingDescription.events, + ]) { + affordanceMap?.entries.forEach(affordances.add); + } + + for (final affordance in affordances) { + final affordanceKey = affordance.key; + final affordanceValue = affordance.value; + + // TODO: Integrate URI variables here + final path = "/$thingId/$affordanceKey"; + final affordanceUri = Uri( + scheme: "http", + host: _server!.address.address, + port: _server!.port, + path: path, + ); + + switch (affordanceValue) { + // TODO: Refactor + // TODO: Handle values from protocol bindings + case Property(:final readOnly, :final writeOnly, :final observable): + if (!writeOnly) { + const operationType = OperationType.readproperty; + final methodName = operationType.defaultHttpMethod; + router.add(methodName, path, (request) async { + final content = await thing.handleReadProperty(affordance.key); + + return Response( + 200, + body: content.body, + headers: { + "Content-Type": content.type, + }, + ); + }); + + affordanceValue.forms.add( + Form( + affordanceUri, + op: const [ + operationType, + ], + ), + ); + } + + if (!readOnly) { + const operationType = OperationType.writeproperty; + final methodName = operationType.defaultHttpMethod; + router.add(methodName, path, (request) async { + if (request is! Request) { + throw Exception(); + } + + final content = Content( + request.mimeType ?? "application/json", + request.read(), + ); + try { + await thing.handleWriteProperty(affordance.key, content); + } on FormatException { + return Response.badRequest(); + } + + return Response( + 204, + ); + }); + + affordanceValue.forms.add( + Form( + affordanceUri, + op: const [ + operationType, + ], + ), + ); + } + + if (observable) { + const _ = [ + OperationType.observeproperty, + OperationType.unobserveproperty, + ]; + + // TODO: Implement some kind of event mechanism (e.g., longpolling) + } + case Action(): + const operationType = OperationType.invokeaction; + final methodName = operationType.defaultHttpMethod; + router.add(methodName, path, (request) async { + if (request is! Request) { + throw Exception(); + } + + final content = Content( + request.mimeType ?? "application/json", + request.read(), + ); + final actionOutput = + await thing.handleInvokeAction(affordance.key, content); + + return Response( + body: actionOutput?.body, + 204, + ); + }); + + affordanceValue.forms.add( + Form( + affordanceUri, + op: const [ + operationType, + ], + ), + ); + + case Event(): + const _ = [ + OperationType.subscribeevent, + OperationType.unsubscribeevent, + ]; + + // TODO: Implement some kind of event mechanism (e.g., longpolling) + } + } + + _routes[thingId] = router; } @override - Future start([ServerSecurityCallback? serverSecurityCallback]) async { - // TODO(JKRhb): implement start - throw UnimplementedError(); + Future start(Servient servient) async { + if (_server != null) { + throw StateError("Server already started"); + } + + _server = await shelf_io.serve(_handleRequest, _bindAddress, port); + + _servient = servient; } @override Future stop() async { - // TODO(JKRhb): implement stop - throw UnimplementedError(); + await _server?.close(); + _server = null; + } + + Future _handleRequest(Request request) async { + final requestedUri = request.requestedUri; + + final firstSegment = requestedUri.pathSegments.firstOrNull; + + final router = _routes[firstSegment]; + + if (router != null) { + return router.call(request); + } + + return Response.notFound("Not found."); + } + + @override + Future destroyThing(ExposableThing thing) async { + final id = thing.thingDescription.id; + + _things.remove(id); + _routes.remove(id); } } diff --git a/lib/src/core/implementation/content_serdes.dart b/lib/src/core/implementation/content_serdes.dart index 424a05e3..0f228a63 100644 --- a/lib/src/core/implementation/content_serdes.dart +++ b/lib/src/core/implementation/content_serdes.dart @@ -173,15 +173,17 @@ class ContentSerdes { Content valueToContent( DataSchemaValue? value, DataSchema? dataSchema, [ - String mediaType = defaultMediaType, + String? mediaType, ]) { + final resolvedMediaType = mediaType ?? defaultMediaType; + _validateValue(value, dataSchema); if (value == null) { - return Content(mediaType, const Stream.empty()); + return Content(resolvedMediaType, const Stream.empty()); } - final parsedMediaType = MediaType.parse(mediaType); + final parsedMediaType = MediaType.parse(resolvedMediaType); final mimeType = parsedMediaType.mimeType; final parameters = parsedMediaType.parameters; @@ -189,7 +191,7 @@ class ContentSerdes { final codec = _getCodecFromMediaType(mimeType) ?? TextCodec(); final bytes = codec.valueToBytes(value, dataSchema, parameters); - return Content(mediaType, Stream.value(bytes)); + return Content(resolvedMediaType, Stream.value(bytes)); } /// Converts a [Content] object to a typed [Object]. diff --git a/lib/src/core/implementation/exposed_thing.dart b/lib/src/core/implementation/exposed_thing.dart index 1a2e6635..655a1360 100644 --- a/lib/src/core/implementation/exposed_thing.dart +++ b/lib/src/core/implementation/exposed_thing.dart @@ -5,82 +5,190 @@ // SPDX-License-Identifier: BSD-3-Clause import "../definitions.dart"; +import "../protocol_interfaces/exposable_thing.dart"; import "../scripting_api.dart" as scripting_api; +import "content.dart"; +import "interaction_output.dart"; import "servient.dart"; /// Implementation of the [scripting_api.ExposedThing] interface. -class ExposedThing implements scripting_api.ExposedThing { - /// Creates a new [ExposedThing] from a [servient] and an [exposedThingInit]. - ExposedThing(this.servient, scripting_api.ExposedThingInit exposedThingInit) +class ExposedThing implements scripting_api.ExposedThing, ExposableThing { + /// Creates a new [ExposedThing] from a [_servient] and an [exposedThingInit]. + ExposedThing(this._servient, scripting_api.ExposedThingInit exposedThingInit) : thingDescription = ThingDescription.fromJson(exposedThingInit); @override final ThingDescription thingDescription; /// The [Servient] associated with this [ExposedThing]. - final Servient servient; + final InternalServient _servient; - /// A [Map] of all the [properties] of this [ExposedThing]. - final Map? properties = {}; + final Map _propertyReadHandlers = + {}; - /// A [Map] of all the [actions] of this [ExposedThing]. - final Map? actions = {}; + final Map _propertyWriteHandlers = + {}; - /// A [Map] of all the [events] of this [ExposedThing]. - final Map? events = {}; + final Map + _propertyObserveHandlers = {}; - @override - Future emitPropertyChange(String name) { - // TODO(JKRhb): implement emitPropertyChange - throw UnimplementedError(); + final Map + _propertyUnobserveHandlers = {}; + + final Map _actionHandlers = {}; + + final Map + _eventSubscribeHandlers = {}; + + final Map + _eventUnsubscribeHandlers = {}; + + final Map _propertyChangeListeners = {}; + + final Map _eventListeners = {}; + + Property _obtainProperty(String name) { + final property = thingDescription.properties?[name]; + + if (property == null) { + throw ArgumentError( + "Property $name does not exist in ExposedThing " + "with title ${thingDescription.title}.", + ); + } + + return property; } - @override - void setPropertyWriteHandler( - String name, - scripting_api.PropertyWriteHandler handler, - ) { - // TODO(JKRhb): implement setPropertyWriteHandler + Event _obtainEvent(String name) { + final event = thingDescription.events?[name]; + + if (event == null) { + throw ArgumentError( + "Event $name does not exist in ExposedThing " + "with title ${thingDescription.title}.", + ); + } + + return event; + } + + void _checkReadableProperty(String name) { + final property = _obtainProperty(name); + + if (property.writeOnly) { + final title = property.title ?? "without title"; + throw ArgumentError("Property $title is not readable."); + } + } + + void _checkWritableProperty(String name) { + final property = _obtainProperty(name); + + if (property.readOnly) { + final title = property.title ?? "without title"; + throw ArgumentError("Property $title is not writable."); + } + } + + void _checkObservableProperty(String name) { + final property = _obtainProperty(name); + + if (!property.observable) { + final title = property.title ?? "without title"; + throw ArgumentError("Property $title is not observable."); + } } @override - Future destroy() { - // TODO(JKRhb): implement destroy - throw UnimplementedError(); + Future emitPropertyChange( + String name, [ + String contentType = "application/json", + ]) async { + final property = _obtainProperty(name); + + final readHandler = _propertyReadHandlers[name]; + + // TODO: Does this need to be a ProtocolListenerRegistry? + final propertyChangeHandler = _propertyChangeListeners[name]; + + // TODO: Do we need to throw an error here? + if (readHandler == null || propertyChangeHandler == null) { + return; + } + + final interactionInput = await readHandler(); + + final content = Content.fromInteractionInput( + interactionInput, + contentType, + _servient.contentSerdes, + property, + ); + + propertyChangeHandler(content); } @override - Future emitEvent(String name, Object? data) { - // TODO(JKRhb): implement emitEvent - throw UnimplementedError(); + Future destroy() async { + _servient.destroyThing(this); } @override - Future expose() { - // TODO(JKRhb): implement expose - throw UnimplementedError(); + Future emitEvent( + String name, + scripting_api.InteractionInput data, [ + String contentType = "application/json", + ]) async { + final event = _obtainEvent(name); + + final eventListener = _eventListeners[name]; + + if (eventListener == null) { + return; + } + + final content = Content.fromInteractionInput( + data, + contentType, + _servient.contentSerdes, + event.data, + ); + + eventListener(content); } + @override + Future expose() => _servient.expose(this); + @override void setActionHandler(String name, scripting_api.ActionHandler handler) { - // TODO(JKRhb): implement setActionHandler + if (thingDescription.actions?[name] == null) { + throw ArgumentError("ExposedThing does not an Action with the key $name"); + } + + _actionHandlers[name] = handler; } @override - void setEventHandler( + void setPropertyReadHandler( String name, - scripting_api.EventListenerHandler handler, + scripting_api.PropertyReadHandler handler, ) { - // TODO(JKRhb): implement setEventHandler + _checkReadableProperty(name); + + _propertyReadHandlers[name] = handler; } @override - void setEventSubscribeHandler( + void setPropertyWriteHandler( String name, - scripting_api.EventSubscriptionHandler handler, + scripting_api.PropertyWriteHandler handler, ) { - // TODO(JKRhb): implement setEventSubscribeHandler + _checkWritableProperty(name); + + _propertyWriteHandlers[name] = handler; } @override @@ -88,23 +196,31 @@ class ExposedThing implements scripting_api.ExposedThing { String name, scripting_api.PropertyReadHandler handler, ) { - // TODO(JKRhb): implement setPropertyObserveHandler + _checkObservableProperty(name); + + _propertyObserveHandlers[name] = handler; } @override - void setPropertyReadHandler( + void setPropertyUnobserveHandler( String name, scripting_api.PropertyReadHandler handler, ) { - // TODO(JKRhb): implement setPropertyReadHandler + _checkObservableProperty(name); + + _propertyUnobserveHandlers[name] = handler; } @override - void setPropertyUnobserveHandler( + void setEventSubscribeHandler( String name, - scripting_api.PropertyReadHandler handler, + scripting_api.EventSubscriptionHandler handler, ) { - // TODO(JKRhb): implement setPropertyUnobserveHandler + if (thingDescription.events?[name] == null) { + throw ArgumentError("ExposedThing does not an Event with the key $name"); + } + + _eventSubscribeHandlers[name] = handler; } @override @@ -112,6 +228,266 @@ class ExposedThing implements scripting_api.ExposedThing { String name, scripting_api.EventSubscriptionHandler handler, ) { - // TODO(JKRhb): implement setEventUnsubscribeHandler + if (thingDescription.events?[name] == null) { + throw ArgumentError("ExposedThing does not an Event with the key $name"); + } + + _eventUnsubscribeHandlers[name] = handler; + } + + @override + Future handleReadProperty( + String propertyName, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final readHandler = _propertyReadHandlers[propertyName]; + + if (readHandler == null) { + throw Exception( + "Read handler for property $propertyName is not defined.", + ); + } + + final interactionInput = await readHandler( + data: data, + uriVariables: uriVariables, + formIndex: formIndex, + ); + + return Content.fromInteractionInput( + interactionInput, + "application/json", + _servient.contentSerdes, + thingDescription.properties?[propertyName], + ); + } + + @override + Future handleWriteProperty( + String propertyName, + Content input, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final writeHandler = _propertyWriteHandlers[propertyName]; + + if (writeHandler == null) { + throw Exception( + "Write handler for property $propertyName is not defined.", + ); + } + + final Form form; + + if (formIndex == null) { + // FIXME: Returning a form does not really make sense here. + form = Form(Uri()); + } else { + form = thingDescription.properties?[propertyName]?.forms + .elementAtOrNull(formIndex) ?? + Form(Uri()); + } + + await writeHandler( + InteractionOutput( + input, + _servient.contentSerdes, + form, + thingDescription.properties?[propertyName], + ), + formIndex: formIndex, + uriVariables: uriVariables, + data: data, + ); + } + + @override + Future handleInvokeAction( + String actionName, + Content input, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final actionHandler = _actionHandlers[actionName]; + + if (actionHandler == null) { + throw Exception( + "Action handler for action $actionName is not defined.", + ); + } + + final action = thingDescription.actions?[actionName]; + + final processedInput = InteractionOutput( + input, + _servient.contentSerdes, + // FIXME: Providing a form does not really make sense here. + Form(Uri()), + action?.input, + ); + + final actionOutput = await actionHandler( + processedInput, + formIndex: formIndex, + uriVariables: uriVariables, + data: data, + ); + + return Content.fromInteractionInput( + actionOutput, + "application/json", + _servient.contentSerdes, + null, + ); + } + + @override + Future handleReadAllProperties({ + int? formIndex, + Map? uriVariables, + Object? data, + }) async => + handleReadMultipleProperties( + thingDescription.properties?.keys.toList() ?? [], + ); + + @override + Future handleReadMultipleProperties( + List propertyNames, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final contentMapEntries = await Future.wait( + propertyNames.map( + (propertyName) async { + final content = await handleReadProperty( + propertyName, + formIndex: formIndex, + uriVariables: uriVariables, + data: data, + ); + + return MapEntry(propertyName, content); + }, + ), + ); + + return Map.fromEntries(contentMapEntries); + } + + @override + Future handleWriteMultipleProperties( + PropertyContentMap inputs, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async => + Future.wait( + inputs.entries.map( + (propertyContentMapEntry) => handleWriteProperty( + propertyContentMapEntry.key, + propertyContentMapEntry.value, + formIndex: formIndex, + uriVariables: uriVariables, + data: data, + ), + ), + ); + + @override + Future handleObserveProperty( + String propertyName, + ContentListener contentListener, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final observeHandler = _propertyObserveHandlers[propertyName]; + + if (observeHandler == null) { + throw Exception( + "Observe handler for property $propertyName is not defined.", + ); + } + + _propertyChangeListeners[propertyName] = contentListener; + + await observeHandler( + data: data, + uriVariables: uriVariables, + formIndex: formIndex, + ); + } + + @override + Future handleUnobserveProperty( + String propertyName, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final unobserveHandler = _propertyUnobserveHandlers[propertyName]; + + if (unobserveHandler == null) { + throw Exception( + "Unobserve handler for property $propertyName is not defined.", + ); + } + + await unobserveHandler( + data: data, + uriVariables: uriVariables, + formIndex: formIndex, + ); + } + + @override + Future handleSubscribeEvent( + String eventName, + ContentListener contentListener, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final subscribeHandler = _eventSubscribeHandlers[eventName]; + + if (subscribeHandler == null) { + throw Exception( + "Observe handler for property $eventName is not defined.", + ); + } + + await subscribeHandler( + data: data, + uriVariables: uriVariables, + formIndex: formIndex, + ); + } + + @override + Future handleUnsubscribeEvent( + String eventName, { + int? formIndex, + Map? uriVariables, + Object? data, + }) async { + final unsubscribeHandler = _eventUnsubscribeHandlers[eventName]; + + if (unsubscribeHandler == null) { + throw Exception( + "Observe handler for property $eventName is not defined.", + ); + } + + await unsubscribeHandler( + data: data, + uriVariables: uriVariables, + formIndex: formIndex, + ); } } diff --git a/lib/src/core/implementation/servient.dart b/lib/src/core/implementation/servient.dart index b63d4028..80f6ce0c 100644 --- a/lib/src/core/implementation/servient.dart +++ b/lib/src/core/implementation/servient.dart @@ -36,12 +36,12 @@ abstract class Servient { /// argument. factory Servient.create({ List? clientFactories, - ServerSecurityCallback? serverSecurityCallback, + List? servers, ContentSerdes? contentSerdes, }) { return InternalServient( clientFactories: clientFactories, - serverSecurityCallback: serverSecurityCallback, + servers: servers, contentSerdes: contentSerdes, ); } @@ -62,38 +62,46 @@ abstract class Servient { /// the return value is `null`. ProtocolClientFactory? removeClientFactory(String scheme); + /// Registers a new [ProtocolServer]. + void addServer(ProtocolServer protocolServer); + + /// De-registers a [ProtocolServer] for the given [scheme], if it exists. + /// + /// If a corresponding [ProtocolServer] was removed, it is return by this + /// method. + bool removeServer(String scheme); + /// Closes this [Servient] and cleans up all resources. Future shutdown(); + + /// The [ContentSerdes] object that is used for serializing/deserializing. + @internal + ContentSerdes get contentSerdes; } /// Provides the internal implementation details of the [Servient] class. class InternalServient implements Servient { /// Creates a new [InternalServient]. InternalServient({ + List? servers, List? clientFactories, - ServerSecurityCallback? serverSecurityCallback, ContentSerdes? contentSerdes, - }) : contentSerdes = contentSerdes ?? ContentSerdes(), - _serverSecurityCallback = serverSecurityCallback { - for (final clientFactory in clientFactories ?? []) { - addClientFactory(clientFactory); - } + }) : contentSerdes = contentSerdes ?? ContentSerdes() { + clientFactories?.forEach(addClientFactory); + servers?.forEach(addServer); } final List _servers = []; final Map _clientFactories = {}; final Map _things = {}; - final ServerSecurityCallback? _serverSecurityCallback; - - /// The [ContentSerdes] object that is used for serializing/deserializing. + @override final ContentSerdes contentSerdes; @override Future start() async { - final serverStatuses = _servers - .map((server) => server.start(_serverSecurityCallback)) - .toList(growable: false); + final serverStatuses = + _servers.map((server) => server.start(this)).toList(growable: false); for (final clientFactory in _clientFactories.values) { clientFactory.init(); @@ -110,9 +118,13 @@ class InternalServient implements Servient { } _clientFactories.clear(); - final serverStatuses = _servers.map((server) => server.stop()).toList(); + final thingDestructionFutures = + [..._things.values].map((exposedThing) => exposedThing.destroy()); + await Future.wait(thingDestructionFutures); + + final serverStatuses = _servers.map((server) => server.stop()); await Future.wait(serverStatuses); - serverStatuses.clear(); + _servers.clear(); } void _cleanUpForms(Iterable? interactionAffordances) { @@ -120,6 +132,7 @@ class InternalServient implements Servient { return; } for (final interactionAffordance in interactionAffordances) { + // FIXME: Properly augment forms interactionAffordance.forms.clear(); } } @@ -130,23 +143,27 @@ class InternalServient implements Servient { return; } - [thing.properties?.values, thing.actions?.values, thing.events?.values] - .forEach(_cleanUpForms); - - final List> serverPromises = []; - for (final server in _servers) { - serverPromises.add(server.expose(thing)); - } - - await Future.wait(serverPromises); + // TODO: Check whether this makes sense. + final thingDescription = thing.thingDescription; + [ + thingDescription.properties?.values, + thingDescription.actions?.values, + thingDescription.events?.values, + ].forEach(_cleanUpForms); + + await Future.wait( + _servers.map( + (server) => server.expose(thing), + ), + ); } /// Adds a [ExposedThing] to the servient if it hasn't been registered before. /// /// Returns `false` if the [thing] has already been registered, otherwise /// `true`. - bool addThing(ExposedThing thing) { - final id = thing.thingDescription.identifier; + bool _addThing(ExposedThing thing) { + final id = thing.thingDescription.id!; if (_things.containsKey(id)) { return false; } @@ -155,25 +172,45 @@ class InternalServient implements Servient { return true; } - /// Returns an [ExposedThing] with the given [id] if it has been registered. - ExposedThing? thing(String id) => _things[id]; + /// Destroys a previously exposed [thing]. + /// + /// Returns `true` if the [thing] was successfully destroyed. + bool destroyThing(ExposedThing thing) { + final id = thing.thingDescription.id; + if (!_things.containsKey(id)) { + return false; + } - /// Returns a [Map] with the [ThingDescription]s of all registered - /// [ExposedThing]s. - Map get thingDescriptions { - return _things.map((key, value) => MapEntry(key, value.thingDescription)); + for (final server in _servers) { + server.destroyThing(thing); + } + _things.remove(id); + return true; } - /// Returns a list of available [ProtocolServer]s. - List get servers => _servers; - - /// Registers a new [ProtocolServer]. + @override void addServer(ProtocolServer server) { _things.values.forEach(server.expose); _servers.add(server); } + @override + bool removeServer(String scheme) { + // TODO: Refactor + final containsScheme = + _servers.where((server) => server.scheme == scheme).isNotEmpty; + + if (containsScheme) { + // TODO: "De-expose" the ExposedThings + _servers.removeWhere((server) => server.scheme == scheme); + + return true; + } + + return false; + } + /// Returns a list of all protocol schemes the registered clients support. List get clientSchemes => _clientFactories.keys.toList(growable: false); @@ -235,7 +272,8 @@ class InternalServient implements Servient { final thingDescription = _expandExposedThingInit(init); final newThing = ExposedThing(this, thingDescription); - if (addThing(newThing)) { + if (_addThing(newThing)) { + await expose(newThing); return newThing; } diff --git a/lib/src/core/protocol_interfaces.dart b/lib/src/core/protocol_interfaces.dart index 8866a08b..cac788f4 100644 --- a/lib/src/core/protocol_interfaces.dart +++ b/lib/src/core/protocol_interfaces.dart @@ -4,6 +4,7 @@ // // SPDX-License-Identifier: BSD-3-Clause +export "protocol_interfaces/exposable_thing.dart"; export "protocol_interfaces/protocol_client.dart"; export "protocol_interfaces/protocol_client_factory.dart"; export "protocol_interfaces/protocol_discoverer.dart"; diff --git a/lib/src/core/protocol_interfaces/exposable_thing.dart b/lib/src/core/protocol_interfaces/exposable_thing.dart new file mode 100644 index 00000000..e44d7359 --- /dev/null +++ b/lib/src/core/protocol_interfaces/exposable_thing.dart @@ -0,0 +1,104 @@ +// 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 "../definitions.dart"; +import "../implementation.dart"; + +/// +typedef PropertyContentMap = Map; + +/// +typedef ContentListener = void Function(Content content); + +/// Interface that allows ProtocolServers to interact with ExposedThings. +// TODO: This needs a better name +abstract interface class ExposableThing { + /// The [ThingDescription] that represents this [ExposableThing]. + ThingDescription get thingDescription; + + /// Handles a `readproperty` operation triggered by a TD consumer. + Future handleReadProperty( + String propertyName, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles a `readmultipleproperties` operation triggered by a TD consumer. + Future handleReadMultipleProperties( + List propertyNames, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles a `readallproperties` operation triggered by a TD consumer. + Future handleReadAllProperties({ + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles a `writeproperty` operation triggered by a TD consumer. + Future handleWriteProperty( + String propertyName, + Content input, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles a `writemultipleproperties` operation triggered by a TD consumer. + Future handleWriteMultipleProperties( + PropertyContentMap inputs, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles an `observeproperty` operation triggered by a TD consumer. + Future handleObserveProperty( + String eventName, + ContentListener contentListener, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles an `unobserveproperty` operation triggered by a TD consumer. + Future handleUnobserveProperty( + String eventName, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles an `invokeaction` operation triggered by a TD consumer. + Future handleInvokeAction( + String propertyName, + Content input, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles a `subscribeevent` operation triggered by a TD consumer. + Future handleSubscribeEvent( + String eventName, + ContentListener contentListener, { + int? formIndex, + Map? uriVariables, + Object? data, + }); + + /// Handles an `unsubscribeevent` operation triggered by a TD consumer. + Future handleUnsubscribeEvent( + String eventName, { + int? formIndex, + Map? uriVariables, + Object? data, + }); +} diff --git a/lib/src/core/protocol_interfaces/protocol_server.dart b/lib/src/core/protocol_interfaces/protocol_server.dart index 8364e595..0be5e9f2 100644 --- a/lib/src/core/protocol_interfaces/protocol_server.dart +++ b/lib/src/core/protocol_interfaces/protocol_server.dart @@ -4,8 +4,9 @@ // // SPDX-License-Identifier: BSD-3-Clause -import "../definitions/credentials/callbacks.dart"; -import "../scripting_api/exposed_thing.dart"; +import "../implementation/exposed_thing.dart"; +import "../implementation/servient.dart"; +import "exposable_thing.dart"; /// Base class for a Protocol Server. abstract interface class ProtocolServer { @@ -15,14 +16,16 @@ abstract interface class ProtocolServer { /// The protocol [scheme] associated with this server. String get scheme; - // TODO(JKRhb): Check if a Servient should be passed as a parameter instead /// Starts the server. Accepts a callback for retrieving a [Map] of /// credentials for [ExposedThing]s at runtime. - Future start([ServerSecurityCallback? serverSecurityCallback]); + Future start(Servient servient); /// Stops the server. Future stop(); /// Exposes a [thing]. - Future expose(ExposedThing thing); + Future expose(ExposableThing thing); + + /// Removes a [thing] from this server. + Future destroyThing(ExposableThing thing); } diff --git a/lib/src/core/scripting_api/exposed_thing.dart b/lib/src/core/scripting_api/exposed_thing.dart index 241e7440..bd0b80b6 100644 --- a/lib/src/core/scripting_api/exposed_thing.dart +++ b/lib/src/core/scripting_api/exposed_thing.dart @@ -4,6 +4,8 @@ // // SPDX-License-Identifier: BSD-3-Clause +import "package:meta/meta.dart"; + import "../definitions.dart"; import "interaction_input.dart"; import "interaction_output.dart"; @@ -88,7 +90,11 @@ abstract interface class ExposedThing { /// Informs all subscribers about the change of the property with the given /// [name]. - Future emitPropertyChange(String name); + @experimental + Future emitPropertyChange( + String name, [ + String contentType = "application/json", + ]); /// Assigns a [handler] function to an action with a given [name]. /// @@ -115,5 +121,10 @@ abstract interface class ExposedThing { /// occurred. /// /// You can provide (optional) input [data] that is emitted with the event. - Future emitEvent(String name, InteractionInput data); + @experimental + Future emitEvent( + String name, + InteractionInput data, [ + String contentType = "application/json", + ]); } diff --git a/pubspec.yaml b/pubspec.yaml index b738ada5..9e1a7f1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,11 +7,9 @@ environment: sdk: '>=3.0.0 <4.0.0' dev_dependencies: - build_runner: ^2.3.0 coverage: ^1.0.4 lint: ^2.0.2 lints: ^4.0.0 - mockito: ^5.0.17 test: ^1.24.3 dependencies: @@ -27,6 +25,8 @@ dependencies: meta: ^1.8.0 mqtt_client: ^10.0.0 multicast_dns: ^0.3.2+1 + shelf: ^1.4.1 + shelf_router: ^1.1.4 typed_data: ^1.3.2 uri: ^1.0.0 uuid: ^4.2.1 diff --git a/test/binding_coap/binding_coap_test.dart b/test/binding_coap/binding_coap_test.dart index 2f391aa6..b202dc4d 100644 --- a/test/binding_coap/binding_coap_test.dart +++ b/test/binding_coap/binding_coap_test.dart @@ -7,11 +7,8 @@ import "package:dart_wot/binding_coap.dart"; import "package:dart_wot/core.dart"; -import "package:mockito/annotations.dart"; import "package:test/test.dart"; -import "binding_coap_test.mocks.dart"; -@GenerateMocks([ExposedThing]) void main() { group("CoAP Binding Tests", () { setUp(() { @@ -20,22 +17,23 @@ void main() { test("Server tests", () { final defaultServer = CoapServer(); + final servient = Servient.create( + servers: [ + defaultServer, + ], + ); expect(defaultServer.port, 5683); expect(defaultServer.scheme, "coap"); expect( - () async => defaultServer.start(), + () async => defaultServer.start(servient), throwsA(const TypeMatcher()), ); expect( () async => defaultServer.stop(), throwsA(const TypeMatcher()), ); - expect( - () async => defaultServer.expose(MockExposedThing()), - throwsA(const TypeMatcher()), - ); final customServer = CoapServer(const CoapConfig(port: 9001, blocksize: 64)); diff --git a/test/binding_coap/binding_coap_test.mocks.dart b/test/binding_coap/binding_coap_test.mocks.dart deleted file mode 100644 index 44078b5e..00000000 --- a/test/binding_coap/binding_coap_test.mocks.dart +++ /dev/null @@ -1,228 +0,0 @@ -// Mocks generated by Mockito 5.4.3 from annotations -// in dart_wot/test/binding_coap/binding_coap_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; - -import 'package:dart_wot/src/core/definitions.dart' as _i2; -import 'package:dart_wot/src/core/scripting_api/exposed_thing.dart' as _i3; -import 'package:dart_wot/src/core/scripting_api/interaction_input.dart' as _i5; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeThingDescription_0 extends _i1.SmartFake - implements _i2.ThingDescription { - _FakeThingDescription_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [ExposedThing]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockExposedThing extends _i1.Mock implements _i3.ExposedThing { - MockExposedThing() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.ThingDescription get thingDescription => (super.noSuchMethod( - Invocation.getter(#thingDescription), - returnValue: _FakeThingDescription_0( - this, - Invocation.getter(#thingDescription), - ), - ) as _i2.ThingDescription); - - @override - _i4.Future expose() => (super.noSuchMethod( - Invocation.method( - #expose, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future destroy() => (super.noSuchMethod( - Invocation.method( - #destroy, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - void setPropertyReadHandler( - String? name, - _i3.PropertyReadHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyReadHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setPropertyWriteHandler( - String? name, - _i3.PropertyWriteHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyWriteHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setPropertyObserveHandler( - String? name, - _i3.PropertyReadHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyObserveHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setPropertyUnobserveHandler( - String? name, - _i3.PropertyReadHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyUnobserveHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - _i4.Future emitPropertyChange(String? name) => (super.noSuchMethod( - Invocation.method( - #emitPropertyChange, - [name], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - void setActionHandler( - String? name, - _i3.ActionHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setActionHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setEventSubscribeHandler( - String? name, - _i3.EventSubscriptionHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setEventSubscribeHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setEventUnsubscribeHandler( - String? name, - _i3.EventSubscriptionHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setEventUnsubscribeHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setEventHandler( - String? name, - _i3.EventListenerHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setEventHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - _i4.Future emitEvent( - String? name, - _i5.InteractionInput? data, - ) => - (super.noSuchMethod( - Invocation.method( - #emitEvent, - [ - name, - data, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); -} diff --git a/test/binding_http/http_test.dart b/test/binding_http/http_test.dart index 997ed1c9..9e509429 100644 --- a/test/binding_http/http_test.dart +++ b/test/binding_http/http_test.dart @@ -7,11 +7,8 @@ import "package:dart_wot/binding_http.dart"; import "package:dart_wot/core.dart"; import "package:dart_wot/src/core/implementation/servient.dart"; -import "package:mockito/annotations.dart"; import "package:test/test.dart"; -import "http_test.mocks.dart"; -@GenerateMocks([ExposedThing]) void main() { group("HTTP tests", () { setUp(() { @@ -24,19 +21,6 @@ void main() { expect(defaultServer.port, 80); expect(defaultServer.scheme, "http"); - expect( - () async => defaultServer.start(), - throwsA(const TypeMatcher()), - ); - expect( - () async => defaultServer.stop(), - throwsA(const TypeMatcher()), - ); - expect( - () async => defaultServer.expose(MockExposedThing()), - throwsA(const TypeMatcher()), - ); - final customServer1 = HttpServer(HttpConfig(secure: true)); expect(customServer1.port, 443); diff --git a/test/binding_http/http_test.mocks.dart b/test/binding_http/http_test.mocks.dart deleted file mode 100644 index 792bcf64..00000000 --- a/test/binding_http/http_test.mocks.dart +++ /dev/null @@ -1,228 +0,0 @@ -// Mocks generated by Mockito 5.4.3 from annotations -// in dart_wot/test/binding_http/http_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; - -import 'package:dart_wot/src/core/definitions.dart' as _i2; -import 'package:dart_wot/src/core/scripting_api/exposed_thing.dart' as _i3; -import 'package:dart_wot/src/core/scripting_api/interaction_input.dart' as _i5; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeThingDescription_0 extends _i1.SmartFake - implements _i2.ThingDescription { - _FakeThingDescription_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [ExposedThing]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockExposedThing extends _i1.Mock implements _i3.ExposedThing { - MockExposedThing() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.ThingDescription get thingDescription => (super.noSuchMethod( - Invocation.getter(#thingDescription), - returnValue: _FakeThingDescription_0( - this, - Invocation.getter(#thingDescription), - ), - ) as _i2.ThingDescription); - - @override - _i4.Future expose() => (super.noSuchMethod( - Invocation.method( - #expose, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future destroy() => (super.noSuchMethod( - Invocation.method( - #destroy, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - void setPropertyReadHandler( - String? name, - _i3.PropertyReadHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyReadHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setPropertyWriteHandler( - String? name, - _i3.PropertyWriteHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyWriteHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setPropertyObserveHandler( - String? name, - _i3.PropertyReadHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyObserveHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setPropertyUnobserveHandler( - String? name, - _i3.PropertyReadHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setPropertyUnobserveHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - _i4.Future emitPropertyChange(String? name) => (super.noSuchMethod( - Invocation.method( - #emitPropertyChange, - [name], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - void setActionHandler( - String? name, - _i3.ActionHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setActionHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setEventSubscribeHandler( - String? name, - _i3.EventSubscriptionHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setEventSubscribeHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setEventUnsubscribeHandler( - String? name, - _i3.EventSubscriptionHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setEventUnsubscribeHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void setEventHandler( - String? name, - _i3.EventListenerHandler? handler, - ) => - super.noSuchMethod( - Invocation.method( - #setEventHandler, - [ - name, - handler, - ], - ), - returnValueForMissingStub: null, - ); - - @override - _i4.Future emitEvent( - String? name, - _i5.InteractionInput? data, - ) => - (super.noSuchMethod( - Invocation.method( - #emitEvent, - [ - name, - data, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); -}