Skip to content

Commit

Permalink
feat!: simplify form augmentation
Browse files Browse the repository at this point in the history
  • Loading branch information
JKRhb committed May 30, 2022
1 parent 7a5eff3 commit aa4c90b
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 283 deletions.
6 changes: 3 additions & 3 deletions lib/src/binding_coap/coap_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class _InternalCoapConfig extends CoapConfigDefault {
}
}

bool get _dtlsNeeded => _form.href.startsWith("coaps");
bool get _dtlsNeeded => _form.resolvedHref.scheme == "coaps";
}

bool _hasPskCredentials(Form form) {
Expand Down Expand Up @@ -164,9 +164,9 @@ class _CoapRequest {
CoapConfig _coapConfig, [
this._subprotocol,
]) : _coapClient = coap.CoapClient(
Uri.parse(_form.href), _InternalCoapConfig(_coapConfig, _form),
_form.resolvedHref, _InternalCoapConfig(_coapConfig, _form),
pskCredentialsCallback: _createPskCallback(_form)),
_requestUri = Uri.parse(_form.href);
_requestUri = _form.resolvedHref;

// TODO(JKRhb): blockwise parameters cannot be handled at the moment due to
// limitations of the CoAP library
Expand Down
2 changes: 1 addition & 1 deletion lib/src/binding_http/http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class HttpClient extends ProtocolClient {
final requestMethod = _getRequestMethod(form, operationType);

final Future<Response> response;
final Uri uri = Uri.parse(form.href);
final Uri uri = form.resolvedHref;
final headers = _getHeadersFromForm(form);
_applySecurityToHeader(form, headers);
final BasicCredentials? basicCredentials =
Expand Down
148 changes: 10 additions & 138 deletions lib/src/core/consumed_thing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
//
// SPDX-License-Identifier: BSD-3-Clause

import 'package:json_schema2/json_schema2.dart';
import 'package:uri/uri.dart';

import '../../scripting_api.dart' as scripting_api;
import '../../scripting_api.dart' hide ConsumedThing, InteractionOutput;
import '../definitions/data_schema.dart';
Expand All @@ -18,18 +15,6 @@ import 'operation_type.dart';
import 'protocol_interfaces/protocol_client.dart';
import 'servient.dart';

/// This [Exception] is thrown when [URI variables] are being used in the [Form]
/// of a TD but no (valid) values were provided.
///
/// [URI variables]: https://www.w3.org/TR/wot-thing-description11/#form-uriVariables
class UriVariableException implements Exception {
/// The error [message].
final String message;

/// Constructor.
UriVariableException(this.message);
}

enum _AffordanceType {
action,
property,
Expand Down Expand Up @@ -66,9 +51,7 @@ class ConsumedThing implements scripting_api.ConsumedThing {

/// Constructor
ConsumedThing(this.servient, this.thingDescription)
: title = thingDescription.title {
_augmentInteractionAffordanceForms();
}
: title = thingDescription.title;

/// Checks if the [Servient] of this [ConsumedThing] supports a protocol
/// [scheme].
Expand All @@ -93,7 +76,7 @@ class ConsumedThing implements scripting_api.ConsumedThing {
if (formIndex != null) {
if (formIndex >= 0 && formIndex < forms.length) {
foundForm = forms[formIndex];
final scheme = Uri.parse(foundForm.href).scheme;
final scheme = foundForm.resolvedHref.scheme;
client = servient.clientFor(scheme);
} else {
throw ArgumentError('ConsumedThing "$title" missing formIndex for '
Expand All @@ -102,19 +85,15 @@ class ConsumedThing implements scripting_api.ConsumedThing {
} else {
foundForm = forms.firstWhere(
(form) =>
hasClientFor(Uri.parse(form.href).scheme) &&
hasClientFor(form.resolvedHref.scheme) &&
_supportsOperationType(form, affordanceType, operationType),
// TODO(JKRhb): Add custom Exception
orElse: () => throw Exception("No matching form found!"));
final scheme = Uri.parse(foundForm.href).scheme;
final scheme = foundForm.resolvedHref.scheme;
client = servient.clientFor(scheme);
}

final credentials = servient.credentials(identifier);

final form = foundForm.copy()
..href = _resolveUriVariables(interactionAffordance, foundForm, options)
..updateCredentials(credentials);
final form = foundForm.resolveUriVariables(options?.uriVariables);

return _ClientAndForm(client, form);
}
Expand All @@ -130,7 +109,7 @@ class ConsumedThing implements scripting_api.ConsumedThing {
}

final clientAndForm = _getClientFor(
property.augmentedForms,
property.forms,
OperationType.readproperty,
_AffordanceType.property,
options,
Expand All @@ -155,7 +134,7 @@ class ConsumedThing implements scripting_api.ConsumedThing {
}

final clientAndForm = _getClientFor(
property.augmentedForms,
property.forms,
OperationType.writeproperty,
_AffordanceType.property,
options,
Expand All @@ -168,97 +147,6 @@ class ConsumedThing implements scripting_api.ConsumedThing {
await client.writeResource(form, content);
}

void _validateUriVariables(
List<String> hrefUriVariables,
Map<String, Object?> affordanceUriVariables,
Map<String, Object?> uriVariables) {
// TODO(JKRhb): Handle global uriVariables

final missingTdDefinitions =
hrefUriVariables.where((element) => !uriVariables.containsKey(element));

if (missingTdDefinitions.isNotEmpty) {
throw UriVariableException("$missingTdDefinitions do not have defined "
"uriVariables in the TD");
}

final missingUserInput = hrefUriVariables
.where((element) => !affordanceUriVariables.containsKey(element));

if (missingUserInput.isNotEmpty) {
throw UriVariableException("$missingUserInput did not have defined "
"Values in the provided InteractionOptions.");
}

// We now assert that all user provided values comply to the Schema
// definition in the TD.
for (final affordanceUriVariable in affordanceUriVariables.entries) {
final key = affordanceUriVariable.key;
final value = affordanceUriVariable.value;

// TODO(JKRhb): Replace with a Draft 7 validator once it is available
// (the original json_schema library which supports Draft 7
// does not support sound null safety, yet, and can therefore
// not be used. json_schema2, on the other hand, only
// supports Draft 6.)
final schema = JsonSchema.createSchema(value);
final valid = schema.validate(uriVariables[key]);

if (!valid) {
throw ArgumentError("Invalid type for URI variable $key");
}
}
}

List<String> _filterUriVariables(String href) {
final regex = RegExp(r"{[?+#./;&]?([^}]*)}");
return regex
.allMatches(Uri.decodeFull(href))
.map((e) => e.group(1))
.whereType<String>()
.toList(growable: false);
}

String _resolveUriVariables(InteractionAffordance interactionAffordance,
Form form, InteractionOptions? options) {
final hrefUriVariables = _filterUriVariables(form.href);
final optionUriVariables = options?.uriVariables;

// Use global URI variables by default and override them with
// affordance-level variables, if any
final Map<String, Object?> affordanceUriVariables = {}
..addAll(thingDescription.uriVariables ?? {})
..addAll(interactionAffordance.uriVariables ?? {});

if (hrefUriVariables.isEmpty) {
// The href uses no uriVariables, therefore we can abort all further
// checks.
return form.href;
}

if (affordanceUriVariables.isEmpty) {
throw UriVariableException("The Form href ${form.href} contains URI "
"variables but the TD does not provide a uriVariables definition.");
}

if (optionUriVariables == null) {
throw ArgumentError("The Form href ${form.href} contains URI variables "
"but no values were provided as InteractionOptions.");
}

// Perform additional validation
_validateUriVariables(
hrefUriVariables, affordanceUriVariables, optionUriVariables);

// As "{" and "}" are "percent encoded" due to Uri.parse(), we need to
// revert the encoding first before we can insert the values.
final decodedHref = Uri.decodeFull(form.href);

// Everything should be okay at this point, we can simply insert the values
// and return the result.
return UriTemplate(decodedHref).expand(optionUriVariables);
}

@override
Future<InteractionOutput> invokeAction(String actionName,
[Object? interactionInput, InteractionOptions? options]) async {
Expand All @@ -269,7 +157,7 @@ class ConsumedThing implements scripting_api.ConsumedThing {
throw StateError('ConsumedThing $title does not have action $actionName');
}

final clientAndForm = _getClientFor(action.augmentedForms,
final clientAndForm = _getClientFor(action.forms,
OperationType.invokeaction, _AffordanceType.action, options, action);

final form = clientAndForm.form;
Expand All @@ -290,22 +178,6 @@ class ConsumedThing implements scripting_api.ConsumedThing {
content, servient.contentSerdes, form, action.output);
}

void _augmentInteractionAffordanceForms() {
final interactionAffordanceList = [
thingDescription.properties,
thingDescription.actions,
thingDescription.events
];

interactionAffordanceList.expand((e) => e.values).forEach(_augmentForms);
}

void _augmentForms(InteractionAffordance interactionAffordance) {
interactionAffordance.augmentedForms = interactionAffordance.forms
.map((form) => form.augment(thingDescription))
.toList(growable: false);
}

@override
Future<Subscription> observeProperty(
String propertyName, scripting_api.InteractionListener listener,
Expand Down Expand Up @@ -350,8 +222,8 @@ class ConsumedThing implements scripting_api.ConsumedThing {
subscriptions = _subscribedEvents;
}

final clientAndForm = _getClientFor(affordance.augmentedForms,
operationType, affordanceType, options, affordance);
final clientAndForm = _getClientFor(
affordance.forms, operationType, affordanceType, options, affordance);

final form = clientAndForm.form;
final client = clientAndForm.client;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/core/servient.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class Servient {
return;
}
for (final interactionAffordance in interactionAffordances) {
interactionAffordance.forms = [];
interactionAffordance.forms.clear();
}
}

Expand Down
7 changes: 6 additions & 1 deletion lib/src/core/thing_discovery.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'dart:async';

import '../../scripting_api.dart' as scripting_api;
import '../definitions/form.dart';
import '../definitions/interaction_affordances/property.dart';
import '../definitions/thing_description.dart';
import 'servient.dart';

Expand Down Expand Up @@ -91,7 +92,11 @@ class ThingDiscovery implements scripting_api.ThingDiscovery {
}
final parsedUri = Uri.parse(uri);
final client = _servient.clientFor(parsedUri.scheme);
final fetchForm = Form(uri, contentType: "application/td+json");
// TODO(JKRhb): Get rid of this workaround
final thingDescription = ThingDescription(null);
final property = Property([], thingDescription);
final fetchForm =
Form(parsedUri, property, contentType: "application/td+json");

final content = await client.readResource(fetchForm);
await client.stop();
Expand Down
Loading

0 comments on commit aa4c90b

Please sign in to comment.