diff --git a/example/exposed_thing/http_server.dart b/example/exposed_thing/http_server.dart index 8c56e94d..4d240c80 100644 --- a/example/exposed_thing/http_server.dart +++ b/example/exposed_thing/http_server.dart @@ -33,6 +33,21 @@ void main() async { ], }, }, + "actions": { + "toggle": { + "input": { + "type": "boolean", + }, + "output": { + "type": "null", + }, + "forms": [ + { + "href": "/toggle", + } + ], + }, + }, }); exposedThing @@ -57,10 +72,21 @@ void main() async { } 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(); @@ -74,5 +100,12 @@ void main() async { 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_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 a855e9ff..5b5bcfff 100644 --- a/lib/src/binding_http/http_server.dart +++ b/lib/src/binding_http/http_server.dart @@ -13,6 +13,7 @@ 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 { @@ -104,7 +105,9 @@ final class HttpServer implements ProtocolServer { // TODO: Handle values from protocol bindings case Property(:final readOnly, :final writeOnly): if (!writeOnly) { - router.get(path, (request) async { + const operationType = OperationType.readproperty; + final methodName = operationType.defaultHttpMethod; + router.add(methodName, path, (request) async { final content = await thing.handleReadProperty(affordance.key); return Response( @@ -120,14 +123,16 @@ final class HttpServer implements ProtocolServer { Form( affordanceUri, op: const [ - OperationType.readproperty, + operationType, ], ), ); } if (!readOnly) { - router.put(path, (request) async { + const operationType = OperationType.writeproperty; + final methodName = operationType.defaultHttpMethod; + router.add(methodName, path, (request) async { if (request is! Request) { throw Exception(); } @@ -151,14 +156,16 @@ final class HttpServer implements ProtocolServer { Form( affordanceUri, op: const [ - OperationType.writeproperty, + operationType, ], ), ); } // TODO: Handle observe case Action(): - router.post(path, (request) async { + const operationType = OperationType.invokeaction; + final methodName = operationType.defaultHttpMethod; + router.add(methodName, path, (request) async { if (request is! Request) { throw Exception(); } @@ -167,13 +174,24 @@ final class HttpServer implements ProtocolServer { request.mimeType ?? "application/json", request.read(), ); - await thing.handleWriteProperty(affordance.key, content); + final blah = + await thing.handleInvokeAction(affordance.key, content); return Response( + body: blah?.body, 204, ); }); + affordanceValue.forms.add( + Form( + affordanceUri, + op: const [ + operationType, + ], + ), + ); + // TODO: Handle observe case Event(): // TODO: Implement diff --git a/lib/src/core/implementation/exposed_thing.dart b/lib/src/core/implementation/exposed_thing.dart index ab6b000e..69281273 100644 --- a/lib/src/core/implementation/exposed_thing.dart +++ b/lib/src/core/implementation/exposed_thing.dart @@ -30,6 +30,8 @@ class ExposedThing implements scripting_api.ExposedThing, ExposableThing { final Map _propertyWriteHandlers = {}; + final Map _actionHandlers = {}; + @override Future emitPropertyChange(String name) { // TODO(JKRhb): implement emitPropertyChange @@ -60,7 +62,7 @@ class ExposedThing implements scripting_api.ExposedThing, ExposableThing { @override void setActionHandler(String name, scripting_api.ActionHandler handler) { - // TODO(JKRhb): implement setActionHandler + _actionHandlers[name] = handler; } @override @@ -180,4 +182,45 @@ class ExposedThing implements scripting_api.ExposedThing, ExposableThing { 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, + ); + } } diff --git a/lib/src/core/protocol_interfaces/exposable_thing.dart b/lib/src/core/protocol_interfaces/exposable_thing.dart index 033340d4..d8945054 100644 --- a/lib/src/core/protocol_interfaces/exposable_thing.dart +++ b/lib/src/core/protocol_interfaces/exposable_thing.dart @@ -29,4 +29,13 @@ abstract interface class ExposableThing { Map? uriVariables, Object? data, }); + + /// Handles a `invokeaction` operation triggered by a TD consumer. + Future handleInvokeAction( + String propertyName, + Content input, { + int? formIndex, + Map? uriVariables, + Object? data, + }); }