diff --git a/lib/src/core/definitions.dart b/lib/src/core/definitions.dart index 82005aef..642038d1 100644 --- a/lib/src/core/definitions.dart +++ b/lib/src/core/definitions.dart @@ -14,6 +14,7 @@ library definitions; export "package:dcaf/dcaf.dart" show AuthServerRequestCreationHint; export "definitions/additional_expected_response.dart"; +export "definitions/context.dart"; export "definitions/credentials/ace_credentials.dart"; export "definitions/credentials/apikey_credentials.dart"; diff --git a/lib/src/core/definitions/data_schema.dart b/lib/src/core/definitions/data_schema.dart index ca9cc0b7..3eb9c2c4 100644 --- a/lib/src/core/definitions/data_schema.dart +++ b/lib/src/core/definitions/data_schema.dart @@ -30,8 +30,8 @@ class DataSchema implements Serializable { this.unit, this.oneOf, this.enumeration, - this.readOnly = false, - this.writeOnly = false, + bool? readOnly, + bool? writeOnly, this.format, this.type, this.minimum, @@ -50,7 +50,8 @@ class DataSchema implements Serializable { this.contentEncoding, this.contentMediaType, this.additionalFields = const {}, - }); + }) : readOnly = readOnly ?? _defaultReadOnly, + writeOnly = writeOnly ?? _defaultWriteOnly; // TODO: Consider creating separate classes for each data type. // Also see https://github.com/w3c/wot-thing-description/issues/1390 @@ -142,6 +143,10 @@ class DataSchema implements Serializable { ); } + static const _defaultWriteOnly = false; + + static const _defaultReadOnly = false; + /// JSON-LD keyword (@type) to label the object with semantic tags (or types). final List? atType; @@ -175,10 +180,10 @@ class DataSchema implements Serializable { final List? enumeration; /// Indicates if a value is read only. - final bool? readOnly; + final bool readOnly; /// Indicates if a value is write only. - final bool? writeOnly; + final bool writeOnly; /// Allows validation based on a format pattern. /// @@ -292,8 +297,6 @@ class DataSchema implements Serializable { ("const", constant), ("default", defaultValue), ("enum", enumeration), - ("readOnly", readOnly), - ("writeOnly", writeOnly), ("format", format), ("unit", unit), ("type", type), @@ -332,6 +335,17 @@ class DataSchema implements Serializable { result[key] = convertedValue; } + final keyValuePairsWithDefault = [ + ("readOnly", readOnly, _defaultReadOnly), + ("writeOnly", writeOnly, _defaultWriteOnly), + ]; + + for (final (key, value, defaultValue) in keyValuePairsWithDefault) { + if (value != defaultValue) { + result[key] = value; + } + } + return result; } } diff --git a/lib/src/core/definitions/form.dart b/lib/src/core/definitions/form.dart index bd514d4c..425cc8e5 100644 --- a/lib/src/core/definitions/form.dart +++ b/lib/src/core/definitions/form.dart @@ -21,7 +21,7 @@ class Form implements Serializable { /// An [href] has to be provided. A [contentType] is optional. Form( this.href, { - this.contentType = "application/json", + this.contentType = _defaultContentType, this.contentCoding, this.subprotocol, this.security, @@ -86,6 +86,8 @@ class Form implements Serializable { ); } + static const _defaultContentType = "application/json"; + /// The [href] pointing to the resource. /// /// Can be a relative or absolute URI. @@ -134,7 +136,6 @@ class Form implements Serializable { Map toJson() { final result = { "href": href.toString(), - "contentType": contentType, ...additionalFields, }; @@ -148,6 +149,10 @@ class Form implements Serializable { op.map((opValue) => opValue.toString()).toList(growable: false); } + if (contentType != _defaultContentType) { + result["contentType"] = contentType; + } + if (contentCoding != null) { result["contentCoding"] = contentCoding; } diff --git a/lib/src/core/definitions/interaction_affordances/property.dart b/lib/src/core/definitions/interaction_affordances/property.dart index 39bde37a..3566e6f2 100644 --- a/lib/src/core/definitions/interaction_affordances/property.dart +++ b/lib/src/core/definitions/interaction_affordances/property.dart @@ -15,7 +15,7 @@ class Property extends InteractionAffordance implements DataSchema { super.uriVariables, super.additionalFields, required this.dataSchema, - this.observable = false, + this.observable = _defaultObservableValue, }); /// Creates a new [Property] from a [json] object. @@ -51,6 +51,22 @@ class Property extends InteractionAffordance implements DataSchema { return property; } + @override + Map toJson() { + final result = { + ...super.toJson(), + ...dataSchema.toJson(), + }; + + if (observable != _defaultObservableValue) { + result["observable"] = observable; + } + + return result; + } + + static const _defaultObservableValue = false; + /// The internal [DataSchema] this property is based on. final DataSchema dataSchema; @@ -85,7 +101,7 @@ class Property extends InteractionAffordance implements DataSchema { List? get oneOf => dataSchema.oneOf; @override - bool get readOnly => dataSchema.readOnly ?? false; + bool get readOnly => dataSchema.readOnly; @override String? get type => dataSchema.type; @@ -94,7 +110,7 @@ class Property extends InteractionAffordance implements DataSchema { String? get unit => dataSchema.unit; @override - bool get writeOnly => dataSchema.writeOnly ?? false; + bool get writeOnly => dataSchema.writeOnly; @override String? get contentEncoding => dataSchema.contentEncoding; @@ -148,8 +164,15 @@ class Property extends InteractionAffordance implements DataSchema { @override Map get additionalFields { - final additionalDataSchemaFields = dataSchema.additionalFields.entries - .where((entry) => super.additionalFields.containsKey(entry.key)); + final additionalDataSchemaFields = + dataSchema.additionalFields.entries.where( + (entry) => [ + ...super.additionalFields.keys, + "observable", + ].contains( + entry.key, + ), + ); return Map.fromEntries(additionalDataSchemaFields); } diff --git a/lib/src/core/definitions/thing_description.dart b/lib/src/core/definitions/thing_description.dart index b88553ea..caf46477 100644 --- a/lib/src/core/definitions/thing_description.dart +++ b/lib/src/core/definitions/thing_description.dart @@ -23,7 +23,7 @@ import "version_info.dart"; @immutable class ThingDescription { /// Creates a new Thing Description object. - const ThingDescription._({ + const ThingDescription({ required this.context, required this.title, required this.security, @@ -114,7 +114,7 @@ class ThingDescription { final additionalFields = json.parseAdditionalFields(prefixMapping, parsedFields); - return ThingDescription._( + return ThingDescription( context: context, title: title, titles: titles, diff --git a/lib/src/core/implementation/servient.dart b/lib/src/core/implementation/servient.dart index d7d57da9..75435b7b 100644 --- a/lib/src/core/implementation/servient.dart +++ b/lib/src/core/implementation/servient.dart @@ -8,7 +8,6 @@ import "package:meta/meta.dart"; import "package:uuid/uuid.dart"; import "../definitions.dart"; -import "../definitions/context.dart"; import "../exceptions.dart"; import "../protocol_interfaces.dart"; import "../scripting_api.dart" as scripting_api; diff --git a/test/core/definitions/serialization_test.dart b/test/core/definitions/serialization_test.dart index 0a7e7f4e..74615d13 100644 --- a/test/core/definitions/serialization_test.dart +++ b/test/core/definitions/serialization_test.dart @@ -38,8 +38,6 @@ void main() { "op": [ "readmultipleproperties", ], - // TODO: Should defaults actually be set? - "contentType": "application/json", }, ], "properties": {}, @@ -103,7 +101,6 @@ void main() { "href": "https://example.org", "subprotocol": "foobar", "contentCoding": "test", - "contentType": "application/json", "security": ["test"], "response": { "contentType": "application/json", @@ -123,7 +120,6 @@ void main() { test("AugmentedForms", () async { final formJson = { "href": "https://example.org", - "contentType": "application/json", }; final thingDescription = { @@ -186,7 +182,6 @@ void main() { "forms": [ { "href": "https://example.org", - "contentType": "application/json", } ], }; diff --git a/test/core/definitions_test.dart b/test/core/definitions_test.dart index 418afe49..1441a44f 100644 --- a/test/core/definitions_test.dart +++ b/test/core/definitions_test.dart @@ -8,7 +8,6 @@ import "dart:convert"; import "package:curie/curie.dart"; import "package:dart_wot/core.dart"; -import "package:dart_wot/src/core/definitions/context.dart"; import "package:dart_wot/src/core/definitions/extensions/json_parser.dart"; import "package:test/test.dart"; diff --git a/test/core/servient_test.dart b/test/core/servient_test.dart index 5d01d403..12ec7070 100644 --- a/test/core/servient_test.dart +++ b/test/core/servient_test.dart @@ -5,7 +5,6 @@ // SPDX-License-Identifier: BSD-3-Clause import "package:dart_wot/core.dart"; -import "package:dart_wot/src/core/definitions/context.dart"; import "package:dart_wot/src/core/implementation/servient.dart"; import "package:test/test.dart"; diff --git a/test/core/thing_description_test.dart b/test/core/thing_description_test.dart index 20c72926..09a03877 100644 --- a/test/core/thing_description_test.dart +++ b/test/core/thing_description_test.dart @@ -39,6 +39,74 @@ void main() { expect(thingDescriptionJson, thingDescription.toJson()); }); + test( + "be able to be created via its constructor and converted to a " + "Map", () { + const thingDescriptionJson = { + "@context": [ + "https://www.w3.org/2022/wot/td/v1.1", + {"@language": "de"}, + ], + "title": "Test Thing", + "securityDefinitions": { + "nosec_sc": {"scheme": "nosec"}, + }, + "security": ["nosec_sc"], + "properties": { + "status": { + "type": "string", + "readOnly": true, + "observable": true, + "forms": [ + { + "href": "https://example.org", + "contentType": "application/cbor", + } + ], + }, + }, + }; + + final thingDescription = ThingDescription( + context: Context( + [ + SingleContextEntry( + Uri.parse("https://www.w3.org/2022/wot/td/v1.1"), + ), + const StringMapContextEntry( + "@language", + "de", + ), + ], + ), + title: "Test Thing", + security: const [ + "nosec_sc", + ], + securityDefinitions: const { + "nosec_sc": NoSecurityScheme(), + }, + properties: { + "status": Property( + forms: [ + Form( + Uri.parse("https://example.org"), + contentType: "application/cbor", + ), + ], + observable: true, + dataSchema: const DataSchema( + type: "string", + readOnly: true, + writeOnly: false, + ), + ), + }, + ); + + expect(thingDescriptionJson, thingDescription.toJson()); + }); + test("throw a FormatException when it is invalid during parsing", () { const thingDescriptionJson = { "@context": [