Skip to content

Commit

Permalink
feat: improve DataSchemaValue handling
Browse files Browse the repository at this point in the history
  • Loading branch information
JKRhb committed Dec 31, 2023
1 parent e6610fe commit ef81993
Show file tree
Hide file tree
Showing 22 changed files with 742 additions and 247 deletions.
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

0 comments on commit ef81993

Please sign in to comment.