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: improve DataSchemaValue handling #86

Merged
merged 7 commits into from
Dec 31, 2023
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
10 changes: 8 additions & 2 deletions example/coap_discovery.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,16 @@ Future<void> handleThingDescription(
ThingDescription thingDescription,
) async {
final consumedThing = await wot.consume(thingDescription);
await consumedThing.writeProperty(propertyName, 'Hello World!');
await consumedThing.writeProperty(
propertyName,
'Hello World'.asInteractionInput(),
);
var output = await consumedThing.readProperty(propertyName);
await output.printValue();
await consumedThing.writeProperty(propertyName, 'Bye World!');
await consumedThing.writeProperty(
propertyName,
'Bye Value'.asInteractionInput(),
);
output = await consumedThing.readProperty(propertyName);
await output.printValue();
}
Expand Down
12 changes: 7 additions & 5 deletions example/mqtt_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,15 @@ Future<void> main(List<String> args) async {
},
);

await consumedThing.invokeAction('toggle', input: 'Hello World!');
await consumedThing.invokeAction('toggle', input: 'Hello World!');
await consumedThing.invokeAction('toggle', input: 'Hello World!');
await consumedThing.invokeAction('toggle', input: 'Hello World!');
final actionInput = 'Hello World'.asInteractionInput();

await consumedThing.invokeAction('toggle', input: actionInput);
await consumedThing.invokeAction('toggle', input: actionInput);
await consumedThing.invokeAction('toggle', input: actionInput);
await consumedThing.invokeAction('toggle', input: actionInput);
await subscription.stop();

await consumedThing.invokeAction('toggle', input: 'Bye World!');
await consumedThing.invokeAction('toggle', input: actionInput);
await consumedThing.readAndPrintProperty('status');
print('Done!');
}
Expand Down
2 changes: 2 additions & 0 deletions lib/scripting_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
library scripting_api;

export 'src/scripting_api/consumed_thing.dart';
export 'src/scripting_api/data_schema_value.dart';
export 'src/scripting_api/discovery/discovery_method.dart';
export 'src/scripting_api/discovery/thing_discovery.dart';
export 'src/scripting_api/discovery/thing_filter.dart';
export 'src/scripting_api/exposed_thing.dart';
export 'src/scripting_api/interaction_input.dart';
export 'src/scripting_api/interaction_output.dart';
export 'src/scripting_api/subscription.dart';
export 'src/scripting_api/types.dart';
Expand Down
18 changes: 13 additions & 5 deletions lib/src/core/codecs/cbor_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,34 @@
import 'package:cbor/cbor.dart' as cbor;

import '../../definitions/data_schema.dart';
import '../../scripting_api/data_schema_value.dart';
import 'content_codec.dart';

/// A [ContentCodec] that encodes and decodes CBOR data.
class CborCodec extends ContentCodec {
@override
List<int> valueToBytes(
Object? value,
DataSchemaValue? dataSchemaValue,
DataSchema? dataSchema,
Map<String, String>? parameters,
) {
return cbor.cborEncode(cbor.CborValue(value));
if (dataSchemaValue == null) {
return [];
}

final cborValue = cbor.CborValue(dataSchemaValue.value);

return cbor.cborEncode(cborValue);
}

@override
Object? bytesToValue(
DataSchemaValue? bytesToValue(
List<int> bytes,
DataSchema? dataSchema,
Map<String, String>? parameters,
) {
// TODO(JKRhb): Use dataSchema for validation
return cbor.cborDecode(bytes).toObject();
final cborObject = cbor.cborDecode(bytes).toObject();

return DataSchemaValue.tryParse(cborObject);
}
}
5 changes: 3 additions & 2 deletions lib/src/core/codecs/content_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
// SPDX-License-Identifier: BSD-3-Clause

import '../../definitions/data_schema.dart';
import '../../scripting_api/data_schema_value.dart';

/// Interface for providing a codec for a specific media type.
abstract class ContentCodec {
/// Converts an [Object] to its byte representation in the given media type.
List<int> valueToBytes(
Object? value,
DataSchemaValue<Object?> value,
DataSchema? dataSchema,
Map<String, String>? parameters,
);

/// Converts a payload of the given media type to an [Object].
Object? bytesToValue(
DataSchemaValue<Object?>? bytesToValue(
List<int> bytes,
DataSchema? dataSchema,
Map<String, String>? parameters,
Expand Down
19 changes: 15 additions & 4 deletions lib/src/core/codecs/json_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,38 @@ import 'dart:convert';

import '../../definitions/data_schema.dart';

import '../../scripting_api/data_schema_value.dart';
import 'content_codec.dart';

/// A [ContentCodec] that encodes and decodes JSON data.
class JsonCodec extends ContentCodec {
@override
List<int> valueToBytes(
Object? value,
DataSchemaValue? dataSchemaValue,
DataSchema? dataSchema,
Map<String, String>? parameters,
) {
return utf8.encode(jsonEncode(value));
if (dataSchemaValue == null) {
return [];
}

return utf8.encode(jsonEncode(dataSchemaValue.value));
}

@override
Object? bytesToValue(
DataSchemaValue? bytesToValue(
List<int> bytes,
DataSchema? dataSchema,
Map<String, String>? parameters,
) {
// TODO(JKRhb): Use dataSchema for validation

return jsonDecode(utf8.decoder.convert(bytes));
if (bytes.isEmpty) {
return null;
}

final decodedJson = jsonDecode(utf8.decoder.convert(bytes));

return DataSchemaValue.tryParse(decodedJson);
}
}
42 changes: 0 additions & 42 deletions lib/src/core/codecs/link_format_codec.dart

This file was deleted.

63 changes: 63 additions & 0 deletions lib/src/core/codecs/text_codec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2023 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 'dart:convert';

import '../../definitions/data_schema.dart';

import '../../scripting_api/data_schema_value.dart';
import 'content_codec.dart';

const _utf8Coding = 'utf-8';

/// A [ContentCodec] that encodes and decodes plain text data.
class TextCodec extends ContentCodec {
@override
List<int> valueToBytes(
DataSchemaValue? dataSchemaValue,
DataSchema? dataSchema,
Map<String, String>? parameters,
) {
if (dataSchemaValue == null) {
return [];
}

final rawValue = dataSchemaValue.value.toString();

final coding = parameters.coding;

switch (coding) {
case _utf8Coding:
return utf8.encode(rawValue);
default:
throw FormatException('Encountered unsupported text coding $coding');
}
}

@override
DataSchemaValue? bytesToValue(
List<int> bytes,
DataSchema? dataSchema,
Map<String, String>? parameters,
) {
if (bytes.isEmpty) {
return null;
}

final coding = parameters.coding;

switch (coding) {
case _utf8Coding:
return DataSchemaValue.fromString(utf8.decoder.convert(bytes));
default:
throw FormatException('Encountered unsupported text coding $coding');
}
}
}

extension _ParametersExtension on Map<String, String>? {
String get coding => this?['charset']?.toLowerCase() ?? _utf8Coding;
}
24 changes: 18 additions & 6 deletions lib/src/core/consumed_thing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../definitions/form.dart';
import '../definitions/interaction_affordances/interaction_affordance.dart';
import '../definitions/operation_type.dart';
import '../definitions/thing_description.dart';
import 'content.dart';
import 'interaction_output.dart';
import 'protocol_interfaces/protocol_client.dart';
import 'servient.dart';
Expand Down Expand Up @@ -153,7 +154,7 @@ class ConsumedThing implements scripting_api.ConsumedThing {
@override
Future<void> writeProperty(
String propertyName,
InteractionInput input, {
InteractionInput? input, {
int? formIndex,
Map<String, Object>? uriVariables,
Object? data,
Expand All @@ -179,15 +180,21 @@ class ConsumedThing implements scripting_api.ConsumedThing {

final form = clientAndForm.form;
final client = clientAndForm.client;
final content = servient.contentSerdes
.valueToContent(input, property, form.contentType);

final content = Content.fromInteractionInput(
input,
form.contentType,
servient.contentSerdes,
property,
);

await client.writeResource(form, content);
}

@override
Future<InteractionOutput> invokeAction(
String actionName, {
InteractionInput input,
InteractionInput? input,
Object? data,
int? formIndex,
Map<String, Object>? uriVariables,
Expand All @@ -213,8 +220,13 @@ class ConsumedThing implements scripting_api.ConsumedThing {

final form = clientAndForm.form;
final client = clientAndForm.client;
final content = servient.contentSerdes
.valueToContent(input, action.input, form.contentType);

final content = Content.fromInteractionInput(
input,
form.contentType,
servient.contentSerdes,
action.input,
);

final output = await client.invokeResource(form, content);

Expand Down
33 changes: 33 additions & 0 deletions lib/src/core/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,45 @@ import 'dart:typed_data';

import 'package:typed_data/typed_data.dart';

import '../definitions/data_schema.dart';
import '../scripting_api/interaction_input.dart';
import 'content_serdes.dart';

/// This class contains binary input or output data and indicates the media
/// type this data is encoded in.
class Content {
/// Creates a new [Content] object from a media [type] and a [body].
Content(this.type, this.body);

/// Creates a new [Content] object from an [interactionInput].
///
/// If the [interactionInput] is not a [StreamInput], it will be converted to
/// a [Stream] by the referenced [contentSerdes] if it supports the specified
/// [contentType].
/// In this case, the optional [dataSchema] will be used for validation before
/// the conversion.
factory Content.fromInteractionInput(
InteractionInput? interactionInput,
String contentType,
ContentSerdes contentSerdes,
DataSchema? dataSchema,
) {
if (interactionInput == null) {
return Content(contentType, const Stream.empty());
}

switch (interactionInput) {
case DataSchemaValueInput():
return contentSerdes.valueToContent(
interactionInput.dataSchemaValue,
dataSchema,
contentType,
);
case StreamInput():
return Content(contentType, interactionInput.byteStream);
}
}

/// The media type corresponding with this [Content] object.
///
/// Examples would be `application/json` or `application/cbor`.
Expand Down
Loading