Skip to content

Commit a65db02

Browse files
authored
Merge pull request #178 from eclipse-thingweb/serialization
feat!: implement proper serialization logic
2 parents e1040ff + 7755c3e commit a65db02

31 files changed

+1065
-54
lines changed

lib/src/binding_coap/coap_client.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ final class CoapClient extends ProtocolClient
284284

285285
return AuthServerRequestCreationHint(
286286
authorizationServer:
287-
aceSecurityScheme.as ?? creationHint?.authorizationServer,
287+
aceSecurityScheme.as?.toString() ?? creationHint?.authorizationServer,
288288
scope: scope ?? creationHint?.scope,
289289
audience: aceSecurityScheme.audience ?? creationHint?.audience,
290290
clientNonce: creationHint?.clientNonce,

lib/src/binding_coap/coap_extensions.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ extension CoapFormExtension on AugmentedForm {
116116
extension CoapExpectedResponseExtension on ExpectedResponse {
117117
T? _obtainVocabularyTerm<T>(String vocabularyTerm) {
118118
final curieString = coapPrefixMapping.expandCurieString(vocabularyTerm);
119-
final formDefinition = additionalFields?[curieString];
119+
final formDefinition = additionalFields[curieString];
120120

121121
if (formDefinition is T) {
122122
return formDefinition;

lib/src/core/definitions/additional_expected_response.dart

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ import "package:curie/curie.dart";
99
import "package:meta/meta.dart";
1010

1111
import "extensions/json_parser.dart";
12+
import "extensions/serializable.dart";
1213

1314
/// Communication metadata describing the expected response message for the
1415
/// primary response.
1516
@immutable
16-
class AdditionalExpectedResponse {
17+
class AdditionalExpectedResponse implements Serializable {
1718
/// Constructs a new [AdditionalExpectedResponse] object from a [contentType].
1819
const AdditionalExpectedResponse(
1920
this.contentType, {
2021
this.schema,
2122
this.success = false,
22-
this.additionalFields,
23+
this.additionalFields = const {},
2324
});
2425

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

6364
/// Any other additional field will be included in this [Map].
64-
final Map<String, dynamic>? additionalFields;
65+
final Map<String, dynamic> additionalFields;
6566

6667
@override
6768
bool operator ==(Object other) {
@@ -79,4 +80,22 @@ class AdditionalExpectedResponse {
7980
@override
8081
int get hashCode =>
8182
Object.hash(success, schema, contentType, additionalFields);
83+
84+
@override
85+
Map<String, dynamic> toJson() {
86+
final result = {
87+
"contentType": contentType,
88+
...additionalFields,
89+
};
90+
91+
if (success) {
92+
result["success"] = success;
93+
}
94+
95+
if (schema != null) {
96+
result["schema"] = schema;
97+
}
98+
99+
return result;
100+
}
82101
}

lib/src/core/definitions/context.dart

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import "package:collection/collection.dart";
88
import "package:curie/curie.dart";
99
import "package:meta/meta.dart";
1010

11+
import "extensions/serializable.dart";
12+
1113
const _tdVersion10ContextUrl = "https://www.w3.org/2019/wot/td/v1";
1214
const _tdVersion11ContextUrl = "https://www.w3.org/2022/wot/td/v1.1";
1315

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

115117
@override
116118
int get hashCode => Object.hashAll(contextEntries);
119+
120+
@override
121+
List<dynamic> toJson() {
122+
final result = <dynamic>[];
123+
final mapResult = <String, String>{};
124+
125+
for (final contextEntry in contextEntries) {
126+
switch (contextEntry) {
127+
case SingleContextEntry(:final uri):
128+
result.add(uri.toString());
129+
case MapContextEntry(:final key, :final value):
130+
//TODO: Could there be duplicate keys?
131+
mapResult[key] = value;
132+
}
133+
}
134+
135+
if (mapResult.isNotEmpty) {
136+
result.add(mapResult);
137+
}
138+
139+
return result;
140+
}
117141
}
118142

119143
/// Base class for `@context` entries.

lib/src/core/definitions/data_schema.dart

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import "package:curie/curie.dart";
88
import "package:meta/meta.dart";
99

1010
import "extensions/json_parser.dart";
11+
import "extensions/json_serializer.dart";
12+
import "extensions/serializable.dart";
1113

1214
/// Metadata that describes the data format used. It can be used for validation.
1315
///
1416
/// See W3C WoT Thing Description specification, [section 5.3.2.1][spec link].
1517
///
1618
/// [spec link]: https://w3c.github.io/wot-thing-description/#dataschema
1719
@immutable
18-
class DataSchema {
20+
class DataSchema implements Serializable {
1921
/// Constructor
2022
const DataSchema({
2123
this.atType,
@@ -47,8 +49,7 @@ class DataSchema {
4749
this.pattern,
4850
this.contentEncoding,
4951
this.contentMediaType,
50-
this.rawJson,
51-
this.additionalFields,
52+
this.additionalFields = const {},
5253
});
5354

5455
// TODO: Consider creating separate classes for each data type.
@@ -83,7 +84,7 @@ class DataSchema {
8384
final minimum = json.parseField<num>("minimum", parsedFields);
8485
final exclusiveMinimum =
8586
json.parseField<num>("exclusiveMinimum", parsedFields);
86-
final maximum = json.parseField<num>("minimum", parsedFields);
87+
final maximum = json.parseField<num>("maximum", parsedFields);
8788
final exclusiveMaximum =
8889
json.parseField<num>("exclusiveMaximum", parsedFields);
8990
final multipleOf = json.parseField<num>("multipleOf", parsedFields);
@@ -136,7 +137,6 @@ class DataSchema {
136137
contentMediaType: contentMediaType,
137138
oneOf: oneOf,
138139
properties: properties,
139-
rawJson: json,
140140
additionalFields: additionalFields,
141141
);
142142
}
@@ -274,8 +274,63 @@ class DataSchema {
274274
final String? contentMediaType;
275275

276276
/// Additional fields that could not be deserialized as class members.
277-
final Map<String, dynamic>? additionalFields;
278-
279-
/// The original JSON object that was parsed when creating this [DataSchema].
280-
final Map<String, dynamic>? rawJson;
277+
final Map<String, dynamic> additionalFields;
278+
279+
@override
280+
Map<String, dynamic> toJson() {
281+
final result = {
282+
...additionalFields,
283+
};
284+
285+
final keyValuePairs = [
286+
("@type", atType),
287+
("title", title),
288+
("titles", titles),
289+
("description", description),
290+
("descriptions", descriptions),
291+
("const", constant),
292+
("default", defaultValue),
293+
("enum", enumeration),
294+
("readOnly", readOnly),
295+
("writeOnly", writeOnly),
296+
("format", format),
297+
("unit", unit),
298+
("type", type),
299+
("minimum", minimum),
300+
("exclusiveMinimum", exclusiveMinimum),
301+
("maximum", maximum),
302+
("exclusiveMaximum", exclusiveMaximum),
303+
("multipleOf", multipleOf),
304+
("items", items),
305+
("minItems", minItems),
306+
("maxItems", maxItems),
307+
("required", required),
308+
("minLength", minLength),
309+
("maxLength", maxLength),
310+
("pattern", pattern),
311+
("contentEncoding", contentEncoding),
312+
("contentMediaType", contentMediaType),
313+
("oneOf", oneOf),
314+
("properties", properties),
315+
];
316+
317+
for (final (key, value) in keyValuePairs) {
318+
final dynamic convertedValue;
319+
320+
switch (value) {
321+
case null:
322+
continue;
323+
case List<Serializable>():
324+
convertedValue = value.toJson();
325+
case Map<String, Serializable>():
326+
convertedValue = value.toJson();
327+
default:
328+
convertedValue = value;
329+
}
330+
331+
result[key] = convertedValue;
332+
}
333+
334+
return result;
335+
}
281336
}

lib/src/core/definitions/expected_response.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ import "package:curie/curie.dart";
88
import "package:meta/meta.dart";
99

1010
import "extensions/json_parser.dart";
11+
import "extensions/serializable.dart";
1112

1213
/// Communication metadata describing the expected response message for the
1314
/// primary response.
1415
@immutable
15-
class ExpectedResponse {
16+
class ExpectedResponse implements Serializable {
1617
/// Constructs a new [ExpectedResponse] object from a [contentType].
1718
const ExpectedResponse(
1819
this.contentType, {
19-
this.additionalFields,
20+
this.additionalFields = const {},
2021
});
2122

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

4041
/// Any other additional field will be included in this [Map].
41-
final Map<String, dynamic>? additionalFields;
42+
final Map<String, dynamic> additionalFields;
43+
44+
@override
45+
Map<String, dynamic> toJson() => {
46+
"contentType": contentType,
47+
...additionalFields,
48+
};
4249
}

lib/src/core/definitions/extensions/json_parser.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ extension ParseField on Map<String, dynamic> {
5555
return fieldValue;
5656
}
5757

58+
if ((T == Map<String, dynamic>) &&
59+
fieldValue is Map &&
60+
fieldValue.isEmpty) {
61+
return <String, dynamic>{} as T;
62+
}
63+
5864
throw FormatException("Expected $T, got ${fieldValue.runtimeType}");
5965
}
6066

@@ -151,6 +157,10 @@ extension ParseField on Map<String, dynamic> {
151157
return null;
152158
}
153159

160+
if (fieldValue is Map && fieldValue.isEmpty) {
161+
return {};
162+
}
163+
154164
if (fieldValue is Map<String, dynamic>) {
155165
final Map<String, T> result = {};
156166

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2024 Contributors to the Eclipse Foundation. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
//
5+
// SPDX-License-Identifier: BSD-3-Clause
6+
7+
import "../form.dart";
8+
import "../link.dart";
9+
import "serializable.dart";
10+
11+
/// Extension that provides JSON serialization for [List]s of [Link]s.
12+
extension SerializableList on List<Serializable> {
13+
/// Converts this [List] of [Serializable] elements to JSON.
14+
List<dynamic> toJson() =>
15+
map((listItem) => listItem.toJson()).toList(growable: false);
16+
}
17+
18+
/// Extension that provides JSON serialization for [List]s of [Form]s.
19+
extension SerializableMap on Map<String, Serializable> {
20+
/// Converts this [Map] of [Serializable] key-value pairs to JSON.
21+
Map<String, dynamic> toJson() =>
22+
map((key, value) => MapEntry(key, value.toJson()));
23+
}
24+
25+
/// Extension that provides JSON serialization for [List]s of [Uri]s.
26+
extension UriListToJsonExtension on List<Uri> {
27+
/// Converts this [List] of [Uri]s to JSON.
28+
List<String> toJson() => map((uri) => uri.toString()).toList(growable: false);
29+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2024 Contributors to the Eclipse Foundation. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
//
5+
// SPDX-License-Identifier: BSD-3-Clause
6+
7+
/// Interface for converting a class object [toJson].
8+
abstract interface class Serializable {
9+
/// Converts this class object into a JSON value.
10+
dynamic toJson();
11+
}

lib/src/core/definitions/form.dart

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import "package:meta/meta.dart";
1010
import "additional_expected_response.dart";
1111
import "expected_response.dart";
1212
import "extensions/json_parser.dart";
13+
import "extensions/serializable.dart";
1314
import "operation_type.dart";
1415

1516
/// Contains the information needed for performing interactions with a Thing.
1617
@immutable
17-
class Form {
18+
class Form implements Serializable {
1819
/// Creates a new [Form] object.
1920
///
2021
/// An [href] has to be provided. A [contentType] is optional.
@@ -128,4 +129,49 @@ class Form {
128129

129130
/// Additional fields collected during the parsing of a JSON object.
130131
final Map<String, dynamic> additionalFields = {};
132+
133+
@override
134+
Map<String, dynamic> toJson() {
135+
final result = {
136+
"href": href.toString(),
137+
"contentType": contentType,
138+
...additionalFields,
139+
};
140+
141+
if (subprotocol != null) {
142+
result["subprotocol"] = subprotocol;
143+
}
144+
145+
final op = this.op;
146+
if (op != null) {
147+
result["op"] =
148+
op.map((opValue) => opValue.toString()).toList(growable: false);
149+
}
150+
151+
if (contentCoding != null) {
152+
result["contentCoding"] = contentCoding;
153+
}
154+
155+
if (security != null) {
156+
result["security"] = security;
157+
}
158+
159+
if (scopes != null) {
160+
result["scopes"] = scopes;
161+
}
162+
163+
final response = this.response;
164+
if (response != null) {
165+
result["response"] = response.toJson();
166+
}
167+
168+
final additionalResponses = this.additionalResponses;
169+
if (additionalResponses != null) {
170+
result["additionalResponses"] = additionalResponses
171+
.map((additionalResponse) => additionalResponse.toJson())
172+
.toList(growable: false);
173+
}
174+
175+
return result;
176+
}
131177
}

0 commit comments

Comments
 (0)