Skip to content

Commit

Permalink
Merge pull request #178 from eclipse-thingweb/serialization
Browse files Browse the repository at this point in the history
feat!: implement proper serialization logic
  • Loading branch information
JKRhb authored Jun 4, 2024
2 parents e1040ff + 7755c3e commit a65db02
Show file tree
Hide file tree
Showing 31 changed files with 1,065 additions and 54 deletions.
2 changes: 1 addition & 1 deletion lib/src/binding_coap/coap_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ final class CoapClient extends ProtocolClient

return AuthServerRequestCreationHint(
authorizationServer:
aceSecurityScheme.as ?? creationHint?.authorizationServer,
aceSecurityScheme.as?.toString() ?? creationHint?.authorizationServer,
scope: scope ?? creationHint?.scope,
audience: aceSecurityScheme.audience ?? creationHint?.audience,
clientNonce: creationHint?.clientNonce,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/binding_coap/coap_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ extension CoapFormExtension on AugmentedForm {
extension CoapExpectedResponseExtension on ExpectedResponse {
T? _obtainVocabularyTerm<T>(String vocabularyTerm) {
final curieString = coapPrefixMapping.expandCurieString(vocabularyTerm);
final formDefinition = additionalFields?[curieString];
final formDefinition = additionalFields[curieString];

if (formDefinition is T) {
return formDefinition;
Expand Down
25 changes: 22 additions & 3 deletions lib/src/core/definitions/additional_expected_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ import "package:curie/curie.dart";
import "package:meta/meta.dart";

import "extensions/json_parser.dart";
import "extensions/serializable.dart";

/// Communication metadata describing the expected response message for the
/// primary response.
@immutable
class AdditionalExpectedResponse {
class AdditionalExpectedResponse implements Serializable {
/// Constructs a new [AdditionalExpectedResponse] object from a [contentType].
const AdditionalExpectedResponse(
this.contentType, {
this.schema,
this.success = false,
this.additionalFields,
this.additionalFields = const {},
});

/// Creates an [AdditionalExpectedResponse] from a [json] object.
Expand Down Expand Up @@ -61,7 +62,7 @@ class AdditionalExpectedResponse {
final String? schema;

/// Any other additional field will be included in this [Map].
final Map<String, dynamic>? additionalFields;
final Map<String, dynamic> additionalFields;

@override
bool operator ==(Object other) {
Expand All @@ -79,4 +80,22 @@ class AdditionalExpectedResponse {
@override
int get hashCode =>
Object.hash(success, schema, contentType, additionalFields);

@override
Map<String, dynamic> toJson() {
final result = {
"contentType": contentType,
...additionalFields,
};

if (success) {
result["success"] = success;
}

if (schema != null) {
result["schema"] = schema;
}

return result;
}
}
26 changes: 25 additions & 1 deletion lib/src/core/definitions/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import "package:collection/collection.dart";
import "package:curie/curie.dart";
import "package:meta/meta.dart";

import "extensions/serializable.dart";

const _tdVersion10ContextUrl = "https://www.w3.org/2019/wot/td/v1";
const _tdVersion11ContextUrl = "https://www.w3.org/2022/wot/td/v1.1";

/// Represents the JSON-LD `@context` of a Thing Description or Thing Model.
@immutable
final class Context {
final class Context implements Serializable {
/// Creates a new context from a list of [contextEntries].
Context(this.contextEntries)
: prefixMapping = _createPrefixMapping(contextEntries);
Expand Down Expand Up @@ -114,6 +116,28 @@ final class Context {

@override
int get hashCode => Object.hashAll(contextEntries);

@override
List<dynamic> toJson() {
final result = <dynamic>[];
final mapResult = <String, String>{};

for (final contextEntry in contextEntries) {
switch (contextEntry) {
case SingleContextEntry(:final uri):
result.add(uri.toString());
case MapContextEntry(:final key, :final value):
//TODO: Could there be duplicate keys?
mapResult[key] = value;
}
}

if (mapResult.isNotEmpty) {
result.add(mapResult);
}

return result;
}
}

/// Base class for `@context` entries.
Expand Down
73 changes: 64 additions & 9 deletions lib/src/core/definitions/data_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import "package:curie/curie.dart";
import "package:meta/meta.dart";

import "extensions/json_parser.dart";
import "extensions/json_serializer.dart";
import "extensions/serializable.dart";

/// Metadata that describes the data format used. It can be used for validation.
///
/// See W3C WoT Thing Description specification, [section 5.3.2.1][spec link].
///
/// [spec link]: https://w3c.github.io/wot-thing-description/#dataschema
@immutable
class DataSchema {
class DataSchema implements Serializable {
/// Constructor
const DataSchema({
this.atType,
Expand Down Expand Up @@ -47,8 +49,7 @@ class DataSchema {
this.pattern,
this.contentEncoding,
this.contentMediaType,
this.rawJson,
this.additionalFields,
this.additionalFields = const {},
});

// TODO: Consider creating separate classes for each data type.
Expand Down Expand Up @@ -83,7 +84,7 @@ class DataSchema {
final minimum = json.parseField<num>("minimum", parsedFields);
final exclusiveMinimum =
json.parseField<num>("exclusiveMinimum", parsedFields);
final maximum = json.parseField<num>("minimum", parsedFields);
final maximum = json.parseField<num>("maximum", parsedFields);
final exclusiveMaximum =
json.parseField<num>("exclusiveMaximum", parsedFields);
final multipleOf = json.parseField<num>("multipleOf", parsedFields);
Expand Down Expand Up @@ -136,7 +137,6 @@ class DataSchema {
contentMediaType: contentMediaType,
oneOf: oneOf,
properties: properties,
rawJson: json,
additionalFields: additionalFields,
);
}
Expand Down Expand Up @@ -274,8 +274,63 @@ class DataSchema {
final String? contentMediaType;

/// Additional fields that could not be deserialized as class members.
final Map<String, dynamic>? additionalFields;

/// The original JSON object that was parsed when creating this [DataSchema].
final Map<String, dynamic>? rawJson;
final Map<String, dynamic> additionalFields;

@override
Map<String, dynamic> toJson() {
final result = {
...additionalFields,
};

final keyValuePairs = [
("@type", atType),
("title", title),
("titles", titles),
("description", description),
("descriptions", descriptions),
("const", constant),
("default", defaultValue),
("enum", enumeration),
("readOnly", readOnly),
("writeOnly", writeOnly),
("format", format),
("unit", unit),
("type", type),
("minimum", minimum),
("exclusiveMinimum", exclusiveMinimum),
("maximum", maximum),
("exclusiveMaximum", exclusiveMaximum),
("multipleOf", multipleOf),
("items", items),
("minItems", minItems),
("maxItems", maxItems),
("required", required),
("minLength", minLength),
("maxLength", maxLength),
("pattern", pattern),
("contentEncoding", contentEncoding),
("contentMediaType", contentMediaType),
("oneOf", oneOf),
("properties", properties),
];

for (final (key, value) in keyValuePairs) {
final dynamic convertedValue;

switch (value) {
case null:
continue;
case List<Serializable>():
convertedValue = value.toJson();
case Map<String, Serializable>():
convertedValue = value.toJson();
default:
convertedValue = value;
}

result[key] = convertedValue;
}

return result;
}
}
13 changes: 10 additions & 3 deletions lib/src/core/definitions/expected_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import "package:curie/curie.dart";
import "package:meta/meta.dart";

import "extensions/json_parser.dart";
import "extensions/serializable.dart";

/// Communication metadata describing the expected response message for the
/// primary response.
@immutable
class ExpectedResponse {
class ExpectedResponse implements Serializable {
/// Constructs a new [ExpectedResponse] object from a [contentType].
const ExpectedResponse(
this.contentType, {
this.additionalFields,
this.additionalFields = const {},
});

/// Creates an [ExpectedResponse] from a [json] object.
Expand All @@ -38,5 +39,11 @@ class ExpectedResponse {
final String contentType;

/// Any other additional field will be included in this [Map].
final Map<String, dynamic>? additionalFields;
final Map<String, dynamic> additionalFields;

@override
Map<String, dynamic> toJson() => {
"contentType": contentType,
...additionalFields,
};
}
10 changes: 10 additions & 0 deletions lib/src/core/definitions/extensions/json_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ extension ParseField on Map<String, dynamic> {
return fieldValue;
}

if ((T == Map<String, dynamic>) &&
fieldValue is Map &&
fieldValue.isEmpty) {
return <String, dynamic>{} as T;
}

throw FormatException("Expected $T, got ${fieldValue.runtimeType}");
}

Expand Down Expand Up @@ -151,6 +157,10 @@ extension ParseField on Map<String, dynamic> {
return null;
}

if (fieldValue is Map && fieldValue.isEmpty) {
return {};
}

if (fieldValue is Map<String, dynamic>) {
final Map<String, T> result = {};

Expand Down
29 changes: 29 additions & 0 deletions lib/src/core/definitions/extensions/json_serializer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 "../form.dart";
import "../link.dart";
import "serializable.dart";

/// Extension that provides JSON serialization for [List]s of [Link]s.
extension SerializableList on List<Serializable> {
/// Converts this [List] of [Serializable] elements to JSON.
List<dynamic> toJson() =>
map((listItem) => listItem.toJson()).toList(growable: false);
}

/// Extension that provides JSON serialization for [List]s of [Form]s.
extension SerializableMap on Map<String, Serializable> {
/// Converts this [Map] of [Serializable] key-value pairs to JSON.
Map<String, dynamic> toJson() =>
map((key, value) => MapEntry(key, value.toJson()));
}

/// Extension that provides JSON serialization for [List]s of [Uri]s.
extension UriListToJsonExtension on List<Uri> {
/// Converts this [List] of [Uri]s to JSON.
List<String> toJson() => map((uri) => uri.toString()).toList(growable: false);
}
11 changes: 11 additions & 0 deletions lib/src/core/definitions/extensions/serializable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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

/// Interface for converting a class object [toJson].
abstract interface class Serializable {
/// Converts this class object into a JSON value.
dynamic toJson();
}
48 changes: 47 additions & 1 deletion lib/src/core/definitions/form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import "package:meta/meta.dart";
import "additional_expected_response.dart";
import "expected_response.dart";
import "extensions/json_parser.dart";
import "extensions/serializable.dart";
import "operation_type.dart";

/// Contains the information needed for performing interactions with a Thing.
@immutable
class Form {
class Form implements Serializable {
/// Creates a new [Form] object.
///
/// An [href] has to be provided. A [contentType] is optional.
Expand Down Expand Up @@ -128,4 +129,49 @@ class Form {

/// Additional fields collected during the parsing of a JSON object.
final Map<String, dynamic> additionalFields = {};

@override
Map<String, dynamic> toJson() {
final result = {
"href": href.toString(),
"contentType": contentType,
...additionalFields,
};

if (subprotocol != null) {
result["subprotocol"] = subprotocol;
}

final op = this.op;
if (op != null) {
result["op"] =
op.map((opValue) => opValue.toString()).toList(growable: false);
}

if (contentCoding != null) {
result["contentCoding"] = contentCoding;
}

if (security != null) {
result["security"] = security;
}

if (scopes != null) {
result["scopes"] = scopes;
}

final response = this.response;
if (response != null) {
result["response"] = response.toJson();
}

final additionalResponses = this.additionalResponses;
if (additionalResponses != null) {
result["additionalResponses"] = additionalResponses
.map((additionalResponse) => additionalResponse.toJson())
.toList(growable: false);
}

return result;
}
}
Loading

0 comments on commit a65db02

Please sign in to comment.