From 5b0f3223a05bb8fc14df45195f6069cb66e9beea Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Sat, 1 Jun 2024 22:54:35 +0200 Subject: [PATCH] feat!: improve deserialization logic --- lib/src/binding_coap/coap_extensions.dart | 2 +- lib/src/binding_mqtt/mqtt_extensions.dart | 2 +- lib/src/core/definitions.dart | 1 - lib/src/core/definitions/context.dart | 10 +- lib/src/core/definitions/data_schema.dart | 11 +- .../definitions/extensions/json_parser.dart | 255 +++-- lib/src/core/definitions/form.dart | 9 +- .../interaction_affordances/action.dart | 7 +- .../interaction_affordances/event.dart | 7 +- .../interaction_affordance.dart | 2 +- .../interaction_affordances/property.dart | 9 +- lib/src/core/definitions/link.dart | 3 +- lib/src/core/definitions/operation_type.dart | 3 +- .../security/ace_security_scheme.dart | 3 +- .../security/combo_security_scheme.dart | 23 +- .../security/oauth2_security_scheme.dart | 3 +- .../core/definitions/thing_description.dart | 51 +- lib/src/core/definitions/thing_model.dart | 2 +- .../validation/thing_description_schema.dart | 923 ------------------ lib/src/core/exceptions.dart | 30 +- .../core/implementation/augmented_form.dart | 11 +- .../core/implementation/content_serdes.dart | 5 +- .../core/implementation/exposed_thing.dart | 3 +- lib/src/core/implementation/servient.dart | 96 +- .../core/implementation/thing_discovery.dart | 2 +- lib/src/core/scripting_api/exposed_thing.dart | 4 +- test/binding_coap/coap_definitions_test.dart | 4 +- test/binding_mqtt/mqtt_extension_test.dart | 2 +- test/core/augmented_form_test.dart | 4 +- test/core/consumed_thing_test.dart | 2 +- test/core/content_serdes_test.dart | 7 +- test/core/context_test.dart | 5 +- test/core/definitions_test.dart | 141 ++- test/core/exceptions_test.dart | 11 +- test/core/servient_test.dart | 95 ++ test/core/thing_description_test.dart | 94 +- 36 files changed, 693 insertions(+), 1149 deletions(-) delete mode 100644 lib/src/core/definitions/validation/thing_description_schema.dart diff --git a/lib/src/binding_coap/coap_extensions.dart b/lib/src/binding_coap/coap_extensions.dart index 1a463c2b..2789d445 100644 --- a/lib/src/binding_coap/coap_extensions.dart +++ b/lib/src/binding_coap/coap_extensions.dart @@ -83,7 +83,7 @@ extension CoapFormExtension on AugmentedForm { return BlockSize.fromDecodedValue(value); // ignore: avoid_catching_errors } on ArgumentError { - throw ValidationException( + throw FormatException( "Encountered invalid blocksize $value in CoAP form", ); } diff --git a/lib/src/binding_mqtt/mqtt_extensions.dart b/lib/src/binding_mqtt/mqtt_extensions.dart index 9adff68e..6607cc0f 100644 --- a/lib/src/binding_mqtt/mqtt_extensions.dart +++ b/lib/src/binding_mqtt/mqtt_extensions.dart @@ -138,7 +138,7 @@ extension MqttFormExtension on AugmentedForm { // TODO: This validation should maybe already happen earlier. if (qosValue != null) { - throw ValidationException( + throw FormatException( "Encountered unknown QoS value $qosValue. " "in form with href $href of Thing Description with Identifier " "$tdIdentifier.", diff --git a/lib/src/core/definitions.dart b/lib/src/core/definitions.dart index 65fb0d45..82005aef 100644 --- a/lib/src/core/definitions.dart +++ b/lib/src/core/definitions.dart @@ -44,4 +44,3 @@ export "definitions/security/psk_security_scheme.dart"; export "definitions/security/security_scheme.dart"; export "definitions/thing_description.dart"; export "definitions/thing_model.dart"; -export "definitions/validation/thing_description_schema.dart"; diff --git a/lib/src/core/definitions/context.dart b/lib/src/core/definitions/context.dart index 54d462ec..c06e9a32 100644 --- a/lib/src/core/definitions/context.dart +++ b/lib/src/core/definitions/context.dart @@ -8,8 +8,6 @@ import "package:collection/collection.dart"; import "package:curie/curie.dart"; import "package:meta/meta.dart"; -import "../exceptions.dart"; - const _tdVersion10ContextUrl = "https://www.w3.org/2019/wot/td/v1"; const _tdVersion11ContextUrl = "https://www.w3.org/2022/wot/td/v1.1"; @@ -30,14 +28,14 @@ final class Context { final firstContextEntry = contextEntries.firstOrNull; if (firstContextEntry is! SingleContextEntry) { - throw const ValidationException("Missing TD context URL."); + throw const FormatException("Missing TD context URL."); } final firstContextValue = firstContextEntry.value; if (![_tdVersion10ContextUrl, _tdVersion11ContextUrl] .contains(firstContextValue)) { - throw ValidationException( + throw FormatException( "Encountered invalid TD context URL $firstContextEntry", ); } @@ -142,12 +140,12 @@ final class SingleContextEntry extends ContextEntry { /// Creates a new [SingleContextEntry] from a [string] that represents a URI. /// /// If the [string] should not be a valid URI, this factory constructor will - /// throw a [ValidationException]. + /// throw a [FormatException]. factory SingleContextEntry.fromString(String string) { final parsedUri = Uri.tryParse(string); if (parsedUri == null) { - throw ValidationException("Encountered invalid URI $string"); + throw FormatException("Encountered invalid URI $string"); } return SingleContextEntry(parsedUri); diff --git a/lib/src/core/definitions/data_schema.dart b/lib/src/core/definitions/data_schema.dart index d2754704..af27337f 100644 --- a/lib/src/core/definitions/data_schema.dart +++ b/lib/src/core/definitions/data_schema.dart @@ -61,7 +61,8 @@ class DataSchema { Set? parsedFields, ]) { parsedFields = parsedFields ?? {}; - final atType = json.parseArrayField("@type", parsedFields); + final atType = + json.parseArrayField("@type", parsedFields: parsedFields); final title = json.parseField("title", parsedFields); final titles = json.parseMapField("titles", parsedFields); final description = json.parseField("description", parsedFields); @@ -69,7 +70,11 @@ class DataSchema { json.parseMapField("descriptions", parsedFields); final constant = json.parseField("constant", parsedFields); final defaultValue = json.parseField("default", parsedFields); - final enumeration = json.parseField>("enum", parsedFields); + final enumeration = json.parseArrayField( + "enum", + parsedFields: parsedFields, + minimalSize: 1, + ); final readOnly = json.parseField("readOnly", parsedFields); final writeOnly = json.parseField("writeOnly", parsedFields); final format = json.parseField("format", parsedFields); @@ -166,7 +171,7 @@ class DataSchema { final List? oneOf; /// Restricted set of values provided as a [List]. - final List? enumeration; + final List? enumeration; /// Indicates if a value is read only. final bool? readOnly; diff --git a/lib/src/core/definitions/extensions/json_parser.dart b/lib/src/core/definitions/extensions/json_parser.dart index fe787631..e9718465 100644 --- a/lib/src/core/definitions/extensions/json_parser.dart +++ b/lib/src/core/definitions/extensions/json_parser.dart @@ -6,7 +6,6 @@ import "package:curie/curie.dart"; -import "../../exceptions.dart"; import "../additional_expected_response.dart"; import "../context.dart"; import "../data_schema.dart"; @@ -37,19 +36,26 @@ extension ParseField on Map { /// Parses a single field with a given [name]. /// - /// Ensures that the field value is of type [T] and returns `null` if the - /// value does not have this type or is not present. + /// If the field is set, the method ensures that its value is of type [T] and + /// throws a [FormatException] otherwise. + /// In case the field is not set, `null` is returned instead, indicating a + /// missing value. /// /// If a [Set] of [parsedFields] is passed to this function, the field [name] - /// will added. This can be used for filtering when parsing additional fields. + /// will be added to it. This can be used for filtering when parsing + /// additional fields. T? parseField(String name, [Set? parsedFields]) { final fieldValue = _processFieldName(name, parsedFields); + if (!containsKey(name)) { + return null; + } + if (fieldValue is T) { return fieldValue; } - return null; + throw FormatException("Expected $T, got ${fieldValue.runtimeType}"); } /// Parses a single field with a given [name] as a [Uri]. @@ -58,7 +64,8 @@ extension ParseField on Map { /// value cannot be parsed as such. /// /// If a [Set] of [parsedFields] is passed to this function, the field [name] - /// will added. This can be used for filtering when parsing additional fields. + /// will be added to it. This can be used for filtering when parsing + /// additional fields. Uri? parseUriField(String name, [Set? parsedFields]) { final fieldValue = parseField(name, parsedFields); @@ -66,7 +73,7 @@ extension ParseField on Map { return null; } - return Uri.tryParse(fieldValue); + return Uri.parse(fieldValue); } /// Parses a single field with a given [name] as a [List] of [Uri]s. @@ -76,8 +83,16 @@ extension ParseField on Map { /// /// If a [Set] of [parsedFields] is passed to this function, the field [name] /// will added. This can be used for filtering when parsing additional fields. - List? parseUriArrayField(String name, [Set? parsedFields]) { - final fieldValue = parseArrayField(name, parsedFields); + List? parseUriArrayField( + String name, { + Set? parsedFields, + int minimalSize = 0, + }) { + final fieldValue = parseArrayField( + name, + parsedFields: parsedFields, + minimalSize: minimalSize, + ); if (fieldValue == null) { return null; @@ -95,30 +110,27 @@ extension ParseField on Map { return result; } - /// Parses a single field with a given [name] and throws a - /// [ValidationException] if the field is not present or does not have the - /// type [T]. + /// Parses a single field with a given [name] and throws a [FormatException] + /// if the field should not be set or not be of type [T]. /// /// Like [parseField], it adds the field [name] to the set of [parsedFields], /// if present. T parseRequiredField(String name, [Set? parsedFields]) { - final fieldValue = parseField(name, parsedFields); + final fieldValue = parseField(name, parsedFields); - if (fieldValue is! T) { - throw ValidationException( - "Value for field $name has wrong data type or is missing. " - "Expected ${T.runtimeType}, got ${fieldValue.runtimeType}.", - ); + if (fieldValue == null) { + throw FormatException("Required field $name is not set."); } return fieldValue; } /// Parses a single field with a given [name] as a [Uri] and throws a - /// [ValidationException] if the field is not present or cannot be parsed. + /// [FormatException] if the field is not present or cannot be parsed. /// /// If a [Set] of [parsedFields] is passed to this function, the field [name] - /// will added. This can be used for filtering when parsing additional fields. + /// will be added. This can be used for filtering when parsing additional + /// fields. Uri parseRequiredUriField(String name, [Set? parsedFields]) { final fieldValue = parseRequiredField(name, parsedFields); @@ -135,39 +147,109 @@ extension ParseField on Map { Map? parseMapField(String name, [Set? parsedFields]) { final fieldValue = _processFieldName(name, parsedFields); + if (!containsKey(name)) { + return null; + } + if (fieldValue is Map) { final Map result = {}; for (final entry in fieldValue.entries) { final value = entry.value; + if (value is T) { result[entry.key] = value; } } - return result; + if (result.length == fieldValue.length) { + return result; + } } - return null; + + throw FormatException( + "Expected ${Map}, got ${fieldValue.runtimeType}", + ); } /// Parses a field with a given [name] that can contain either a single value /// or a list of values of type [T]. /// - /// Ensures that the field value is of type [T] or `List` and returns - /// `null` if the value does not have one of these types or is not present. + /// Ensures that the field value is either of type [T] or of type [List], + /// and throws a [FormatException] otherwise. + /// If the field is unset, `null` will be returned instead. /// /// If a [Set] of [parsedFields] is passed to this function, the field [name] - /// will added. This can be used for filtering when parsing additional fields. - List? parseArrayField(String name, [Set? parsedFields]) { + /// will be added. This can be used for filtering when parsing additional + /// fields. + List? parseArrayField( + String name, { + Set? parsedFields, + int minimalSize = 0, + }) { final fieldValue = parseField(name, parsedFields); - if (fieldValue is T) { - return [fieldValue]; + if (fieldValue == null) { + return null; + } + + final List result; + + if (fieldValue is List) { + result = fieldValue; + } else if (fieldValue is T) { + result = [fieldValue]; } else if (fieldValue is List) { - return fieldValue.whereType().toList(growable: false); + final filteredArray = fieldValue.whereType().toList(growable: false); + + if (filteredArray.length == fieldValue.length) { + result = filteredArray; + } else { + throw FormatException( + "Expected $T or a List of $T, but found a List member with invalid " + "type", + ); + } + } else { + throw FormatException( + "Expected $T or a List of $T, got ${fieldValue.runtimeType}", + ); } - return null; + if (result.length < minimalSize) { + throw const FormatException( + "Expected a non-empty array, but encountered an empty one.", + ); + } + + return result; + } + + /// Parses a field with a given [name] that can contain either a single value + /// or a list of values of type [T]. + /// + /// Ensures that the field value is either of type [T] or of type [List], + /// and throws a [FormatException] otherwise. + /// + /// If a [Set] of [parsedFields] is passed to this function, the field [name] + /// will be added. This can be used for filtering when parsing additional + /// fields. + List parseRequiredArrayField( + String name, { + Set? parsedFields, + int minimalSize = 0, + }) { + final result = parseArrayField( + name, + parsedFields: parsedFields, + minimalSize: minimalSize, + ); + + if (result == null) { + throw FormatException("Missing required field $name"); + } + + return result; } /// Parses a field with a given [name] as a [DataSchema]. @@ -182,13 +264,13 @@ extension ParseField on Map { PrefixMapping prefixMapping, Set? parsedFields, ) { - final fieldValue = parseField(name, parsedFields); + final fieldValue = parseField>(name, parsedFields); - if (fieldValue is Map) { - return DataSchema.fromJson(fieldValue, prefixMapping); + if (fieldValue == null) { + return null; } - return null; + return DataSchema.fromJson(fieldValue, prefixMapping); } /// Parses a field with a given [name] as a [List] of [DataSchema]s. @@ -203,15 +285,12 @@ extension ParseField on Map { PrefixMapping prefixMapping, Set? parsedFields, ) { - final fieldValue = parseField(name, parsedFields); - - if (fieldValue is List>) { - return fieldValue - .map((e) => DataSchema.fromJson(e, prefixMapping)) - .toList(); - } + final fieldValue = + parseField>>(name, parsedFields); - return null; + return fieldValue + ?.map((e) => DataSchema.fromJson(e, prefixMapping)) + .toList(); } /// Parses a field with a given [name] as a [Map] of [DataSchema]s. @@ -226,20 +305,26 @@ extension ParseField on Map { PrefixMapping prefixMapping, Set? parsedFields, ) { - final fieldValue = parseField(name, parsedFields); + final fieldValue = parseField>(name, parsedFields); - if (fieldValue is Map>) { - return Map.fromEntries( - fieldValue.entries.map( - (entry) => MapEntry( - entry.key, - DataSchema.fromJson(entry.value, prefixMapping), - ), - ), - ); + if (fieldValue == null) { + return null; } - return null; + final result = {}; + for (final entry in fieldValue.entries) { + final value = entry.value; + + if (value is Map) { + result[entry.key] = DataSchema.fromJson(value, prefixMapping); + } else { + throw FormatException( + "Expected a Map, got ${value.runtimeType}", + ); + } + } + + return result; } /// Parses [Form]s contained in this JSON object. @@ -250,15 +335,14 @@ extension ParseField on Map { PrefixMapping prefixMapping, Set? parsedFields, ) { - final fieldValue = parseField("forms", parsedFields); - - if (fieldValue is! List) { - return null; - } + final fieldValue = parseArrayField>( + "forms", + parsedFields: parsedFields, + minimalSize: 1, + ); return fieldValue - .whereType>() - .map( + ?.map( (e) => Form.fromJson( e, prefixMapping, @@ -280,13 +364,13 @@ extension ParseField on Map { parsedFields, ); - if (forms != null) { - return forms; + if (forms == null) { + throw const FormatException( + 'Missing "forms" member in InteractionAffordance', + ); } - throw const ValidationException( - 'Missing "forms" member in InteractionAffordance', - ); + return forms; } /// Parses [Link]s contained in this JSON object. @@ -296,16 +380,10 @@ extension ParseField on Map { PrefixMapping prefixMapping, Set? parsedFields, ) { - final fieldValue = parseField("links", parsedFields); - - if (fieldValue is! List) { - return null; - } + final fieldValue = + parseField>>("links", parsedFields); - return fieldValue - .whereType>() - .map((e) => Link.fromJson(e, prefixMapping)) - .toList(); + return fieldValue?.map((e) => Link.fromJson(e, prefixMapping)).toList(); } /// Parses [SecurityScheme]s contained in this JSON object. @@ -386,7 +464,7 @@ extension ParseField on Map { final Map result = {}; for (final property in fieldValue.entries) { - final dynamic value = property.value; + final value = property.value; if (value is Map) { result[property.key] = Property.fromJson(value, prefixMapping); } @@ -411,7 +489,7 @@ extension ParseField on Map { final Map result = {}; for (final property in fieldValue.entries) { - final dynamic value = property.value; + final value = property.value; if (value is Map) { result[property.key] = Action.fromJson(value, prefixMapping); } @@ -436,7 +514,7 @@ extension ParseField on Map { final Map result = {}; for (final property in fieldValue.entries) { - final dynamic value = property.value; + final value = property.value; if (value is Map) { result[property.key] = Event.fromJson(value, prefixMapping); } @@ -448,9 +526,14 @@ extension ParseField on Map { /// Processes this JSON value and tries to generate a [List] of /// [OperationType]s from it. List? parseOperationTypes( - Set? parsedFields, - ) { - final opArray = parseArrayField("op", parsedFields); + Set? parsedFields, { + int minimalSize = 0, + }) { + final opArray = parseArrayField( + "op", + parsedFields: parsedFields, + minimalSize: minimalSize, + ); return opArray?.map(OperationType.fromString).toList(); } @@ -478,11 +561,13 @@ extension ParseField on Map { List? parseAdditionalExpectedResponse( PrefixMapping prefixMapping, String formContentType, - Set? parsedFields, - ) { + Set? parsedFields, { + int minimalSize = 0, + }) { final fieldValue = parseArrayField>( "additionalResponses", - parsedFields, + parsedFields: parsedFields, + minimalSize: minimalSize, ); if (fieldValue == null) { @@ -610,7 +695,7 @@ Iterable _parseContextEntries(dynamic json) sync* { final value = entry.value; if (value is! String) { - throw ValidationException( + throw FormatException( "Expected $value to be a String or a Map " "as @context entry, got ${value.runtimeType} instead."); } @@ -625,7 +710,7 @@ Iterable _parseContextEntries(dynamic json) sync* { }); } default: - throw ValidationException( + throw FormatException( "Expected the @context entry $json to " "either be a String or a Map, " "got ${json.runtimeType} instead.", diff --git a/lib/src/core/definitions/form.dart b/lib/src/core/definitions/form.dart index 137602f7..8df6818a 100644 --- a/lib/src/core/definitions/form.dart +++ b/lib/src/core/definitions/form.dart @@ -53,8 +53,13 @@ class Form { final contentCoding = json.parseField("contentCoding", parsedFields); - final security = json.parseArrayField("security", parsedFields); - final scopes = json.parseArrayField("scopes", parsedFields); + final security = json.parseArrayField( + "security", + parsedFields: parsedFields, + minimalSize: 1, + ); + final scopes = + json.parseArrayField("scopes", parsedFields: parsedFields); final response = json.parseExpectedResponse(prefixMapping, parsedFields); final additionalResponses = json.parseAdditionalExpectedResponse( diff --git a/lib/src/core/definitions/interaction_affordances/action.dart b/lib/src/core/definitions/interaction_affordances/action.dart index 24c02837..31d87162 100644 --- a/lib/src/core/definitions/interaction_affordances/action.dart +++ b/lib/src/core/definitions/interaction_affordances/action.dart @@ -37,8 +37,11 @@ final class Action extends InteractionAffordance { final description = json.parseField("description", parsedFields); final descriptions = json.parseMapField("descriptions", parsedFields); - final uriVariables = - json.parseMapField("uriVariables", parsedFields); + final uriVariables = json.parseDataSchemaMapField( + "uriVariables", + prefixMapping, + parsedFields, + ); final safe = json.parseField("safe", parsedFields) ?? false; final idempotent = diff --git a/lib/src/core/definitions/interaction_affordances/event.dart b/lib/src/core/definitions/interaction_affordances/event.dart index ea060f22..f42eb8a2 100644 --- a/lib/src/core/definitions/interaction_affordances/event.dart +++ b/lib/src/core/definitions/interaction_affordances/event.dart @@ -34,8 +34,11 @@ class Event extends InteractionAffordance { final description = json.parseField("description", parsedFields); final descriptions = json.parseMapField("descriptions", parsedFields); - final uriVariables = - json.parseMapField("uriVariables", parsedFields); + final uriVariables = json.parseDataSchemaMapField( + "uriVariables", + prefixMapping, + parsedFields, + ); final subscription = json.parseDataSchemaField("subscription", prefixMapping, parsedFields); diff --git a/lib/src/core/definitions/interaction_affordances/interaction_affordance.dart b/lib/src/core/definitions/interaction_affordances/interaction_affordance.dart index 62db05db..2556cfdc 100644 --- a/lib/src/core/definitions/interaction_affordances/interaction_affordance.dart +++ b/lib/src/core/definitions/interaction_affordances/interaction_affordance.dart @@ -51,7 +51,7 @@ sealed class InteractionAffordance { /// URI template variables as defined in [RFC 6570]. /// /// [RFC 6570]: http://tools.ietf.org/html/rfc6570 - final Map? uriVariables; + final Map? uriVariables; /// Additional fields that could not be deserialized as class members. final Map? additionalFields; diff --git a/lib/src/core/definitions/interaction_affordances/property.dart b/lib/src/core/definitions/interaction_affordances/property.dart index 5dfa7ffe..3b197f98 100644 --- a/lib/src/core/definitions/interaction_affordances/property.dart +++ b/lib/src/core/definitions/interaction_affordances/property.dart @@ -26,8 +26,11 @@ class Property extends InteractionAffordance implements DataSchema { final Set parsedFields = {}; final observable = json.parseField("observable", parsedFields) ?? false; - final uriVariables = - json.parseMapField("uriVariables", parsedFields); + final uriVariables = json.parseDataSchemaMapField( + "uriVariables", + prefixMapping, + parsedFields, + ); final dataSchema = DataSchema.fromJson(json, prefixMapping, parsedFields); final forms = json.parseAffordanceForms( prefixMapping, @@ -73,7 +76,7 @@ class Property extends InteractionAffordance implements DataSchema { Object? get defaultValue => dataSchema?.defaultValue; @override - List? get enumeration => dataSchema?.enumeration; + List? get enumeration => dataSchema?.enumeration; @override String? get format => dataSchema?.format; diff --git a/lib/src/core/definitions/link.dart b/lib/src/core/definitions/link.dart index c12ade1c..3ea92c63 100644 --- a/lib/src/core/definitions/link.dart +++ b/lib/src/core/definitions/link.dart @@ -39,7 +39,8 @@ class Link { final rel = json.parseField("rel", parsedFields); final anchor = json.parseUriField("anchor", parsedFields); final sizes = json.parseField("sizes", parsedFields); - final hreflang = json.parseArrayField("hreflang", parsedFields); + final hreflang = + json.parseArrayField("hreflang", parsedFields: parsedFields); final additionalFields = json.parseAdditionalFields(prefixMapping, parsedFields); diff --git a/lib/src/core/definitions/operation_type.dart b/lib/src/core/definitions/operation_type.dart index 90d0b325..f23c0fc6 100644 --- a/lib/src/core/definitions/operation_type.dart +++ b/lib/src/core/definitions/operation_type.dart @@ -4,7 +4,6 @@ // // SPDX-License-Identifier: BSD-3-Clause -import "../exceptions.dart"; import "interaction_affordances/interaction_affordance.dart"; /// Enumeration for the possible WoT operation types. @@ -55,7 +54,7 @@ enum OperationType { final operationType = OperationType._registry[stringValue]; if (operationType == null) { - throw ValidationException( + throw FormatException( "Encountered unknown OperationType $stringValue.", ); } diff --git a/lib/src/core/definitions/security/ace_security_scheme.dart b/lib/src/core/definitions/security/ace_security_scheme.dart index 06c1fab2..a02aa249 100644 --- a/lib/src/core/definitions/security/ace_security_scheme.dart +++ b/lib/src/core/definitions/security/ace_security_scheme.dart @@ -43,7 +43,8 @@ final class AceSecurityScheme extends SecurityScheme { final as = json.parseField("ace:as", parsedFields); final cnonce = json.parseField("ace:cnonce", parsedFields); final audience = json.parseField("ace:audience", parsedFields); - final scopes = json.parseArrayField("ace:scopes", parsedFields); + final scopes = + json.parseArrayField("ace:scopes", parsedFields: parsedFields); final additionalFields = json.parseAdditionalFields(prefixMapping, parsedFields); diff --git a/lib/src/core/definitions/security/combo_security_scheme.dart b/lib/src/core/definitions/security/combo_security_scheme.dart index d0aec36d..8fac24c3 100644 --- a/lib/src/core/definitions/security/combo_security_scheme.dart +++ b/lib/src/core/definitions/security/combo_security_scheme.dart @@ -4,6 +4,7 @@ // // SPDX-License-Identifier: BSD-3-Clause +import "package:collection/collection.dart"; import "package:curie/curie.dart"; import "../extensions/json_parser.dart"; @@ -38,8 +39,26 @@ final class ComboSecurityScheme extends SecurityScheme { final jsonLdType = json.parseArrayField("@type"); final proxy = json.parseUriField("proxy", parsedFields); - final oneOf = json.parseArrayField("oneOf", parsedFields); - final allOf = json.parseArrayField("allOf", parsedFields); + final oneOf = json.parseArrayField( + "oneOf", + parsedFields: parsedFields, + minimalSize: 2, + ); + final allOf = json.parseArrayField( + "allOf", + parsedFields: parsedFields, + minimalSize: 2, + ); + + final count = + [oneOf, allOf].whereNotNull().fold(0, (previous, _) => previous + 1); + + if (count != 1) { + throw FormatException( + "Expected exactly one of allOf or oneOf to be " + "defined, but $count were given.", + ); + } final additionalFields = json.parseAdditionalFields(prefixMapping, parsedFields); diff --git a/lib/src/core/definitions/security/oauth2_security_scheme.dart b/lib/src/core/definitions/security/oauth2_security_scheme.dart index fd827e38..657d55da 100644 --- a/lib/src/core/definitions/security/oauth2_security_scheme.dart +++ b/lib/src/core/definitions/security/oauth2_security_scheme.dart @@ -46,7 +46,8 @@ final class OAuth2SecurityScheme extends SecurityScheme { json.parseField("authorization", parsedFields); final token = json.parseField("token", parsedFields); final refresh = json.parseField("refresh", parsedFields); - final scopes = json.parseArrayField("scopes", parsedFields); + final scopes = + json.parseArrayField("scopes", parsedFields: parsedFields); final flow = json.parseRequiredField("flow", parsedFields); final additionalFields = diff --git a/lib/src/core/definitions/thing_description.dart b/lib/src/core/definitions/thing_description.dart index 0ca2aa9f..1ba82fbc 100644 --- a/lib/src/core/definitions/thing_description.dart +++ b/lib/src/core/definitions/thing_description.dart @@ -7,7 +7,6 @@ import "package:curie/curie.dart"; import "package:meta/meta.dart"; -import "../exceptions.dart"; import "additional_expected_response.dart"; import "context.dart"; import "data_schema.dart"; @@ -17,7 +16,6 @@ import "interaction_affordances/interaction_affordance.dart"; import "link.dart"; import "security/security_scheme.dart"; import "thing_model.dart"; -import "validation/thing_description_schema.dart"; import "version_info.dart"; /// Represents a WoT Thing Description @@ -52,27 +50,16 @@ class ThingDescription { }) : _rawThingDescription = rawThingDescription; /// Creates a [ThingDescription] from a [json] object. - - factory ThingDescription.fromJson( - Map json, { - bool validate = true, - }) { - if (validate) { - final validationResult = thingDescriptionSchema.validate(json); - if (!validationResult.isValid) { - throw ValidationException( - "Validation of Thing Description failed.", - validationResult.errors, - ); - } - } - + /// + /// Throws a [FormatException] if the Thing Description should not be valid. + factory ThingDescription.fromJson(Map json) { final Set parsedFields = {}; final context = json.parseContext(parsedFields); final prefixMapping = context.prefixMapping; - final atType = json.parseArrayField("@type", parsedFields); + final atType = + json.parseArrayField("@type", parsedFields: parsedFields); final title = json.parseRequiredField("title", parsedFields); final titles = json.parseMapField("titles", parsedFields); final description = json.parseField("description", parsedFields); @@ -85,13 +72,22 @@ class ThingDescription { final base = json.parseUriField("base", parsedFields); final id = json.parseField("id", parsedFields); - final security = - json.parseArrayField("security", parsedFields) ?? []; + final security = json.parseRequiredArrayField( + "security", + parsedFields: parsedFields, + ); final securityDefinitions = json.parseSecurityDefinitions(prefixMapping, parsedFields) ?? {}; final forms = json.parseForms(prefixMapping, parsedFields); + // TODO: Move somewhere else + // TODO: Validate correct use of op-values + forms?.forEach((form) { + if (form.op == null) { + throw const FormatException('Missing "op" field in thing-level form.'); + } + }); final properties = json.parseProperties(prefixMapping, parsedFields); final actions = json.parseActions(prefixMapping, parsedFields); @@ -99,14 +95,21 @@ class ThingDescription { final links = json.parseLinks(prefixMapping, parsedFields); - final profile = json.parseUriArrayField("profile", parsedFields); + final profile = json.parseUriArrayField( + "profile", + parsedFields: parsedFields, + minimalSize: 1, + ); final schemaDefinitions = json.parseDataSchemaMapField( "schemaDefinitions", prefixMapping, parsedFields, ); - final uriVariables = - json.parseMapField("uriVariables", parsedFields); + final uriVariables = json.parseDataSchemaMapField( + "uriVariables", + prefixMapping, + parsedFields, + ); final additionalFields = json.parseAdditionalFields(prefixMapping, parsedFields); @@ -235,7 +238,7 @@ class ThingDescription { /// URI template variables as defined in [RFC 6570]. /// /// [RFC 6570]: http://tools.ietf.org/html/rfc6570 - final Map? uriVariables; + final Map? uriVariables; /// Additional fields collected during the parsing of a JSON object. final Map? additionalFields; diff --git a/lib/src/core/definitions/thing_model.dart b/lib/src/core/definitions/thing_model.dart index e7efa1c9..6dd576e9 100644 --- a/lib/src/core/definitions/thing_model.dart +++ b/lib/src/core/definitions/thing_model.dart @@ -8,7 +8,7 @@ import "extensions/json_parser.dart"; /// Class representing a WoT Thing Model. /// -/// See W3C WoT Thing Description Specificition, [section 10][spec link]. +/// See W3C WoT Thing Description Specification, [section 10][spec link]. /// /// [spec link]: https://w3c.github.io/wot-thing-description/#thing-model class ThingModel { diff --git a/lib/src/core/definitions/validation/thing_description_schema.dart b/lib/src/core/definitions/validation/thing_description_schema.dart deleted file mode 100644 index 5c4e184c..00000000 --- a/lib/src/core/definitions/validation/thing_description_schema.dart +++ /dev/null @@ -1,923 +0,0 @@ -// Copyright 2022 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 "package:json_schema/json_schema.dart"; - -/// JSON Schema definition used for validating Thing Descriptions. -final thingDescriptionSchema = JsonSchema.create(_rawThingDescriptionSchema); - -final Map _rawThingDescriptionSchema = { - "title": "Thing Description", - "version": "1.1-09-November-2023", - "description": - "JSON Schema for validating TD instances against the TD information " - "model. TD instances can be with or without terms that have default " - "values", - "\$schema": "http://json-schema.org/draft-07/schema#", - "\$id": - "https://raw.githubusercontent.com/w3c/wot-thing-description/main/validation/td-json-schema-validation.json", - "definitions": { - "anyUri": {"type": "string"}, - "description": {"type": "string"}, - "descriptions": { - "type": "object", - "additionalProperties": {"type": "string"}, - }, - "title": {"type": "string"}, - "titles": { - "type": "object", - "additionalProperties": {"type": "string"}, - }, - "security": { - "oneOf": [ - { - "type": "array", - "items": {"type": "string"}, - "minItems": 1, - }, - {"type": "string"}, - ], - }, - "scopes": { - "oneOf": [ - { - "type": "array", - "items": {"type": "string"}, - }, - {"type": "string"}, - ], - }, - "subprotocol": { - "type": "string", - "examples": ["longpoll", "websub", "sse"], - }, - "thing-context-td-uri-v1": { - "type": "string", - "const": "https://www.w3.org/2019/wot/td/v1", - }, - "thing-context-td-uri-v1.1": { - "type": "string", - "const": "https://www.w3.org/2022/wot/td/v1.1", - }, - "thing-context-td-uri-temp": { - "type": "string", - "const": "http://www.w3.org/ns/td", - }, - "thing-context": { - "anyOf": [ - { - "\$comment": - "New context URI with other vocabularies after it but not the " - "old one", - "type": "array", - "items": [ - {"\$ref": "#/definitions/thing-context-td-uri-v1.1"}, - ], - "additionalItems": { - "anyOf": [ - {"\$ref": "#/definitions/anyUri"}, - {"type": "object"}, - ], - "not": {"\$ref": "#/definitions/thing-context-td-uri-v1"}, - }, - }, - { - "\$comment": "Only the new context URI", - "\$ref": "#/definitions/thing-context-td-uri-v1.1", - }, - { - "\$comment": - "Old context URI, followed by the new one and possibly other " - "vocabularies. minItems and contains are required since " - "prefixItems does not say all items should be provided", - "type": "array", - "prefixItems": [ - {"\$ref": "#/definitions/thing-context-td-uri-v1"}, - {"\$ref": "#/definitions/thing-context-td-uri-v1.1"}, - ], - "minItems": 2, - "contains": {"\$ref": "#/definitions/thing-context-td-uri-v1.1"}, - "additionalItems": { - "anyOf": [ - {"\$ref": "#/definitions/anyUri"}, - {"type": "object"}, - ], - }, - }, - { - "\$comment": - "Old context URI, followed by possibly other vocabularies. " - "minItems and contains are required since prefixItems does " - "not say all items should be provided", - "type": "array", - "prefixItems": [ - {"\$ref": "#/definitions/thing-context-td-uri-v1"}, - ], - "minItems": 1, - "contains": {"\$ref": "#/definitions/thing-context-td-uri-v1"}, - "additionalItems": { - "anyOf": [ - {"\$ref": "#/definitions/anyUri"}, - {"type": "object"}, - ], - }, - }, - { - "\$comment": "Only the old context URI", - "\$ref": "#/definitions/thing-context-td-uri-v1", - } - ], - }, - "bcp47_string": { - "type": "string", - "pattern": - "^(((([A-Za-z]{2,3}(-([A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|" - "[A-Za-z]{5,8})(-([A-Za-z]{4}))?(-([A-Za-z]{2}|[0-9]{3}))?" - "(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-([0-9A-WY-Za-wy-z]" - "(-[A-Za-z0-9]{2,8})+))*(-(x(-[A-Za-z0-9]{1,8})+))?)|" - "(x(-[A-Za-z0-9]{1,8})+)|((en-GB-oed|i-ami|i-bnn|i-default|" - "i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|" - "i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|" - "cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|" - "zh-xiang)))\$", - }, - "type_declaration": { - "oneOf": [ - { - "type": "string", - "not": {"const": "tm:ThingModel"}, - }, - { - "type": "array", - "items": { - "type": "string", - "not": {"const": "tm:ThingModel"}, - }, - } - ], - }, - "dataSchema-type": { - "type": "string", - "enum": [ - "boolean", - "integer", - "number", - "string", - "object", - "array", - "null", - ], - }, - "dataSchema": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "title": {"\$ref": "#/definitions/title"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "titles": {"\$ref": "#/definitions/titles"}, - "writeOnly": {"type": "boolean"}, - "readOnly": {"type": "boolean"}, - "oneOf": { - "type": "array", - "items": {"\$ref": "#/definitions/dataSchema"}, - }, - "unit": {"type": "string"}, - "enum": {"type": "array", "minItems": 1, "uniqueItems": true}, - "format": {"type": "string"}, - "const": {}, - "default": {}, - "contentEncoding": {"type": "string"}, - "contentMediaType": {"type": "string"}, - "type": {"\$ref": "#/definitions/dataSchema-type"}, - "items": { - "oneOf": [ - {"\$ref": "#/definitions/dataSchema"}, - { - "type": "array", - "items": {"\$ref": "#/definitions/dataSchema"}, - } - ], - }, - "maxItems": {"type": "integer", "minimum": 0}, - "minItems": {"type": "integer", "minimum": 0}, - "minimum": {"type": "number"}, - "maximum": {"type": "number"}, - "exclusiveMinimum": {"type": "number"}, - "exclusiveMaximum": {"type": "number"}, - "minLength": {"type": "integer", "minimum": 0}, - "maxLength": {"type": "integer", "minimum": 0}, - "multipleOf": {"\$ref": "#/definitions/multipleOfDefinition"}, - "properties": { - "additionalProperties": {"\$ref": "#/definitions/dataSchema"}, - }, - "required": { - "type": "array", - "items": {"type": "string"}, - }, - }, - }, - "additionalResponsesDefinition": { - "type": "array", - "items": { - "type": "object", - "properties": { - "contentType": {"type": "string"}, - "schema": {"type": "string"}, - "success": {"type": "boolean"}, - }, - }, - }, - "multipleOfDefinition": { - "type": ["integer", "number"], - "exclusiveMinimum": 0, - }, - "expectedResponse": { - "type": "object", - "properties": { - "contentType": {"type": "string"}, - }, - "required": ["contentType"], - }, - "form_element_base": { - "type": "object", - "properties": { - "op": { - "oneOf": [ - {"type": "string"}, - { - "type": "array", - "items": {"type": "string"}, - } - ], - }, - "href": {"\$ref": "#/definitions/anyUri"}, - "contentType": {"type": "string"}, - "contentCoding": {"type": "string"}, - "subprotocol": {"\$ref": "#/definitions/subprotocol"}, - "security": {"\$ref": "#/definitions/security"}, - "scopes": {"\$ref": "#/definitions/scopes"}, - "response": {"\$ref": "#/definitions/expectedResponse"}, - "additionalResponses": { - "\$ref": "#/definitions/additionalResponsesDefinition", - }, - }, - "required": ["href"], - "additionalProperties": true, - }, - "form_element_property": { - "allOf": [ - {"\$ref": "#/definitions/form_element_base"}, - ], - "type": "object", - "properties": { - "op": { - "oneOf": [ - { - "type": "string", - "enum": [ - "readproperty", - "writeproperty", - "observeproperty", - "unobserveproperty", - ], - }, - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "readproperty", - "writeproperty", - "observeproperty", - "unobserveproperty", - ], - }, - "minItems": 1, - } - ], - }, - }, - "additionalProperties": true, - }, - "form_element_action": { - "allOf": [ - {"\$ref": "#/definitions/form_element_base"}, - ], - "type": "object", - "properties": { - "op": { - "oneOf": [ - { - "type": "string", - "enum": ["invokeaction", "queryaction", "cancelaction"], - }, - { - "type": "array", - "items": { - "type": "string", - "enum": ["invokeaction", "queryaction", "cancelaction"], - }, - "minItems": 1, - } - ], - }, - }, - "additionalProperties": true, - }, - "form_element_event": { - "allOf": [ - {"\$ref": "#/definitions/form_element_base"}, - ], - "type": "object", - "properties": { - "op": { - "oneOf": [ - { - "type": "string", - "enum": ["subscribeevent", "unsubscribeevent"], - }, - { - "type": "array", - "items": { - "type": "string", - "enum": ["subscribeevent", "unsubscribeevent"], - }, - "minItems": 1, - } - ], - }, - }, - "additionalProperties": true, - }, - "form_element_root": { - "allOf": [ - {"\$ref": "#/definitions/form_element_base"}, - ], - "type": "object", - "properties": { - "op": { - "oneOf": [ - { - "type": "string", - "enum": [ - "readallproperties", - "writeallproperties", - "readmultipleproperties", - "writemultipleproperties", - "observeallproperties", - "unobserveallproperties", - "queryallactions", - "subscribeallevents", - "unsubscribeallevents", - ], - }, - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "readallproperties", - "writeallproperties", - "readmultipleproperties", - "writemultipleproperties", - "observeallproperties", - "unobserveallproperties", - "queryallactions", - "subscribeallevents", - "unsubscribeallevents", - ], - }, - "minItems": 1, - } - ], - }, - }, - "additionalProperties": true, - "required": ["op"], - }, - "form": { - "\$comment": - "This is NOT for validation purposes but for automatic generation of TS types. For more info, please see: https://github.com/w3c/wot-thing-description/pull/1319#issuecomment-994950057", - "oneOf": [ - {"\$ref": "#/definitions/form_element_property"}, - {"\$ref": "#/definitions/form_element_action"}, - {"\$ref": "#/definitions/form_element_event"}, - {"\$ref": "#/definitions/form_element_root"}, - ], - }, - "property_element": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "title": {"\$ref": "#/definitions/title"}, - "titles": {"\$ref": "#/definitions/titles"}, - "forms": { - "type": "array", - "minItems": 1, - "items": {"\$ref": "#/definitions/form_element_property"}, - }, - "uriVariables": { - "type": "object", - "additionalProperties": {"\$ref": "#/definitions/dataSchema"}, - }, - "observable": {"type": "boolean"}, - "writeOnly": {"type": "boolean"}, - "readOnly": {"type": "boolean"}, - "oneOf": { - "type": "array", - "items": {"\$ref": "#/definitions/dataSchema"}, - }, - "unit": {"type": "string"}, - "enum": {"type": "array", "minItems": 1, "uniqueItems": true}, - "format": {"type": "string"}, - "const": {}, - "default": {}, - "type": {"\$ref": "#/definitions/dataSchema-type"}, - "items": { - "oneOf": [ - {"\$ref": "#/definitions/dataSchema"}, - { - "type": "array", - "items": {"\$ref": "#/definitions/dataSchema"}, - } - ], - }, - "maxItems": {"type": "integer", "minimum": 0}, - "minItems": {"type": "integer", "minimum": 0}, - "minimum": {"type": "number"}, - "maximum": {"type": "number"}, - "exclusiveMinimum": {"type": "number"}, - "exclusiveMaximum": {"type": "number"}, - "minLength": {"type": "integer", "minimum": 0}, - "maxLength": {"type": "integer", "minimum": 0}, - "multipleOf": {"\$ref": "#/definitions/multipleOfDefinition"}, - "properties": { - "additionalProperties": {"\$ref": "#/definitions/dataSchema"}, - }, - "required": { - "type": "array", - "items": {"type": "string"}, - }, - }, - "required": ["forms"], - "additionalProperties": true, - }, - "action_element": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "title": {"\$ref": "#/definitions/title"}, - "titles": {"\$ref": "#/definitions/titles"}, - "forms": { - "type": "array", - "minItems": 1, - "items": {"\$ref": "#/definitions/form_element_action"}, - }, - "uriVariables": { - "type": "object", - "additionalProperties": {"\$ref": "#/definitions/dataSchema"}, - }, - "input": {"\$ref": "#/definitions/dataSchema"}, - "output": {"\$ref": "#/definitions/dataSchema"}, - "safe": {"type": "boolean"}, - "idempotent": {"type": "boolean"}, - "synchronous": {"type": "boolean"}, - }, - "required": ["forms"], - "additionalProperties": true, - }, - "event_element": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "title": {"\$ref": "#/definitions/title"}, - "titles": {"\$ref": "#/definitions/titles"}, - "forms": { - "type": "array", - "minItems": 1, - "items": {"\$ref": "#/definitions/form_element_event"}, - }, - "uriVariables": { - "type": "object", - "additionalProperties": {"\$ref": "#/definitions/dataSchema"}, - }, - "subscription": {"\$ref": "#/definitions/dataSchema"}, - "data": {"\$ref": "#/definitions/dataSchema"}, - "dataResponse": {"\$ref": "#/definitions/dataSchema"}, - "cancellation": {"\$ref": "#/definitions/dataSchema"}, - }, - "required": ["forms"], - "additionalProperties": true, - }, - "base_link_element": { - "type": "object", - "properties": { - "href": {"\$ref": "#/definitions/anyUri"}, - "type": {"type": "string"}, - "rel": {"type": "string"}, - "anchor": {"\$ref": "#/definitions/anyUri"}, - "hreflang": { - "anyOf": [ - {"\$ref": "#/definitions/bcp47_string"}, - { - "type": "array", - "items": {"\$ref": "#/definitions/bcp47_string"}, - } - ], - }, - }, - "required": ["href"], - "additionalProperties": true, - }, - "link_element": { - "allOf": [ - {"\$ref": "#/definitions/base_link_element"}, - { - "not": { - "description": "A basic link element should not contain sizes", - "type": "object", - "properties": {"sizes": {}}, - "required": ["sizes"], - }, - }, - { - "not": { - "description": - "A basic link element should not contain icon or tm:extends", - "properties": { - "rel": { - "enum": ["icon", "tm:extends"], - }, - }, - "required": ["rel"], - }, - } - ], - }, - "icon_link_element": { - "allOf": [ - {"\$ref": "#/definitions/base_link_element"}, - { - "properties": { - "rel": {"const": "icon"}, - "sizes": {"type": "string", "pattern": "[0-9]*x[0-9]+"}, - }, - "required": ["rel"], - } - ], - }, - "additionalSecurityScheme": { - "description": - "Applies to additional SecuritySchemes not defined in the WoT TD " - "specification.", - "\$comment": - "Additional SecuritySchemes should always be defined via a context " - "extension, using a prefixed value for the scheme. This prefix " - "(e.g. 'ace', see the example below) must contain at least one " - "character in order to reference a valid JSON-LD context " - "extension.", - "examples": [ - { - "scheme": "ace:ACESecurityScheme", - "ace:as": "coaps://as.example.com/token", - "ace:audience": "coaps://rs.example.com", - "ace:scopes": ["limited", "special"], - "ace:cnonce": true, - } - ], - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": {"type": "string", "pattern": ".+:.*"}, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "noSecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["nosec"], - }, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "autoSecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["auto"], - }, - }, - "not": { - "required": ["name"], - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "comboSecurityScheme": { - "oneOf": [ - { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["combo"], - }, - "oneOf": { - "type": "array", - "minItems": 2, - "items": {"type": "string"}, - }, - }, - "required": ["scheme", "oneOf"], - "additionalProperties": true, - }, - { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["combo"], - }, - "allOf": { - "type": "array", - "minItems": 2, - "items": {"type": "string"}, - }, - }, - "required": ["scheme", "allOf"], - "additionalProperties": true, - } - ], - }, - "basicSecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["basic"], - }, - "in": { - "type": "string", - "enum": ["header", "query", "body", "cookie", "auto"], - }, - "name": {"type": "string"}, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "digestSecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["digest"], - }, - "qop": { - "type": "string", - "enum": ["auth", "auth-int"], - }, - "in": { - "type": "string", - "enum": ["header", "query", "body", "cookie", "auto"], - }, - "name": {"type": "string"}, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "apiKeySecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["apikey"], - }, - "in": { - "type": "string", - "enum": ["header", "query", "body", "cookie", "uri", "auto"], - }, - "name": {"type": "string"}, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "bearerSecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["bearer"], - }, - "authorization": {"\$ref": "#/definitions/anyUri"}, - "alg": {"type": "string"}, - "format": {"type": "string"}, - "in": { - "type": "string", - "enum": ["header", "query", "body", "cookie", "auto"], - }, - "name": {"type": "string"}, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "pskSecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["psk"], - }, - "identity": {"type": "string"}, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "oAuth2SecurityScheme": { - "type": "object", - "properties": { - "@type": {"\$ref": "#/definitions/type_declaration"}, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "proxy": {"\$ref": "#/definitions/anyUri"}, - "scheme": { - "type": "string", - "enum": ["oauth2"], - }, - "authorization": {"\$ref": "#/definitions/anyUri"}, - "token": {"\$ref": "#/definitions/anyUri"}, - "refresh": {"\$ref": "#/definitions/anyUri"}, - "scopes": { - "oneOf": [ - { - "type": "array", - "items": {"type": "string"}, - }, - {"type": "string"}, - ], - }, - "flow": { - "anyOf": [ - {"type": "string"}, - { - "type": "string", - "enum": ["code", "client"], - } - ], - }, - }, - "required": ["scheme"], - "additionalProperties": true, - }, - "securityScheme": { - "oneOf": [ - {"\$ref": "#/definitions/noSecurityScheme"}, - {"\$ref": "#/definitions/autoSecurityScheme"}, - {"\$ref": "#/definitions/comboSecurityScheme"}, - {"\$ref": "#/definitions/basicSecurityScheme"}, - {"\$ref": "#/definitions/digestSecurityScheme"}, - {"\$ref": "#/definitions/apiKeySecurityScheme"}, - {"\$ref": "#/definitions/bearerSecurityScheme"}, - {"\$ref": "#/definitions/pskSecurityScheme"}, - {"\$ref": "#/definitions/oAuth2SecurityScheme"}, - {"\$ref": "#/definitions/additionalSecurityScheme"}, - ], - }, - }, - "type": "object", - "properties": { - "id": {"type": "string", "format": "uri"}, - "title": {"\$ref": "#/definitions/title"}, - "titles": {"\$ref": "#/definitions/titles"}, - "properties": { - "type": "object", - "additionalProperties": {"\$ref": "#/definitions/property_element"}, - }, - "actions": { - "type": "object", - "additionalProperties": {"\$ref": "#/definitions/action_element"}, - }, - "events": { - "type": "object", - "additionalProperties": {"\$ref": "#/definitions/event_element"}, - }, - "description": {"\$ref": "#/definitions/description"}, - "descriptions": {"\$ref": "#/definitions/descriptions"}, - "version": { - "type": "object", - "properties": { - "instance": {"type": "string"}, - }, - "required": ["instance"], - }, - "links": { - "type": "array", - "items": { - "oneOf": [ - {"\$ref": "#/definitions/link_element"}, - {"\$ref": "#/definitions/icon_link_element"}, - ], - }, - }, - "forms": { - "type": "array", - "minItems": 1, - "items": {"\$ref": "#/definitions/form_element_root"}, - }, - "base": {"\$ref": "#/definitions/anyUri"}, - "securityDefinitions": { - "type": "object", - "minProperties": 1, - "additionalProperties": {"\$ref": "#/definitions/securityScheme"}, - }, - "schemaDefinitions": { - "type": "object", - "minProperties": 1, - "additionalProperties": {"\$ref": "#/definitions/dataSchema"}, - }, - "support": {"\$ref": "#/definitions/anyUri"}, - "created": {"type": "string", "format": "date-time"}, - "modified": {"type": "string", "format": "date-time"}, - "profile": { - "oneOf": [ - {"\$ref": "#/definitions/anyUri"}, - { - "type": "array", - "minItems": 1, - "items": {"\$ref": "#/definitions/anyUri"}, - } - ], - }, - "security": { - "oneOf": [ - {"type": "string"}, - { - "type": "array", - "minItems": 1, - "items": {"type": "string"}, - } - ], - }, - "uriVariables": { - "type": "object", - "additionalProperties": {"\$ref": "#/definitions/dataSchema"}, - }, - "@type": {"\$ref": "#/definitions/type_declaration"}, - "@context": {"\$ref": "#/definitions/thing-context"}, - }, - "required": ["title", "security", "securityDefinitions", "@context"], - "additionalProperties": true, -}; diff --git a/lib/src/core/exceptions.dart b/lib/src/core/exceptions.dart index 60d5fb2b..25d472d5 100644 --- a/lib/src/core/exceptions.dart +++ b/lib/src/core/exceptions.dart @@ -14,7 +14,7 @@ base class DartWotException implements Exception { /// Constructor. const DartWotException(this.message); - /// The error message of this [ValidationException]. + /// The error message of this [DartWotException]. final String message; /// The name of this [Exception] that will appear in the error message log. @@ -24,34 +24,6 @@ base class DartWotException implements Exception { String toString() => "$exceptionType: $message"; } -/// An [Exception] that is thrown when the validation of a definition fails. -base class ValidationException extends DartWotException { - /// Constructor. - const ValidationException(super.message, [this._validationErrors]); - - final List? _validationErrors; - - @override - String get exceptionType => "ValidationException"; - - @override - String toString() { - final String formattedValidationErrors; - - final validationErrors = _validationErrors; - if (validationErrors != null) { - formattedValidationErrors = [ - "\n\nErrors:\n", - ...validationErrors, - ].join("\n"); - } else { - formattedValidationErrors = ""; - } - - return "$exceptionType: $message$formattedValidationErrors"; - } -} - /// Custom [Exception] that is thrown when the discovery process fails. final class DiscoveryException extends DartWotException { /// Creates a new [DiscoveryException] with the specified error [message]. diff --git a/lib/src/core/implementation/augmented_form.dart b/lib/src/core/implementation/augmented_form.dart index 78bc4ab0..58340ec5 100644 --- a/lib/src/core/implementation/augmented_form.dart +++ b/lib/src/core/implementation/augmented_form.dart @@ -10,7 +10,6 @@ import "package:meta/meta.dart"; import "package:uri/uri.dart"; import "../definitions.dart"; -import "../exceptions.dart"; /// A [Form] augmented with information from its associated [_thingDescription] /// and [_interactionAffordance]. @@ -108,7 +107,7 @@ final class AugmentedForm implements Form { return href; } - final Map affordanceUriVariables = { + final Map affordanceUriVariables = { ..._thingDescription.uriVariables ?? {}, ..._interactionAffordance.uriVariables ?? {}, }; @@ -133,14 +132,14 @@ final class AugmentedForm implements Form { void _validateUriVariables( List uriVariablesInHref, - Map affordanceUriVariables, + Map affordanceUriVariables, Map userProvidedUriVariables, ) { final uncoveredHrefUriVariables = uriVariablesInHref .where((element) => !affordanceUriVariables.containsKey(element)); if (uncoveredHrefUriVariables.isNotEmpty) { - throw ValidationException( + throw FormatException( "The following URI template variables defined in the form's href " "but are not covered by a uriVariable entry at the TD or affordance " "level: ${uncoveredHrefUriVariables.join(", ")}."); @@ -158,11 +157,11 @@ final class AugmentedForm implements Form { } final schemaValue = affordanceUriVariable.value; - final schema = JsonSchema.create(schemaValue); + final schema = JsonSchema.create(schemaValue.rawJson ?? {}); final result = schema.validate(userProvidedValue); if (!result.isValid) { - throw ValidationException("Invalid type for URI variable $key"); + throw FormatException("Invalid type for URI variable $key"); } } } diff --git a/lib/src/core/implementation/content_serdes.dart b/lib/src/core/implementation/content_serdes.dart index f3475c16..e534344f 100644 --- a/lib/src/core/implementation/content_serdes.dart +++ b/lib/src/core/implementation/content_serdes.dart @@ -10,7 +10,6 @@ import "package:http_parser/http_parser.dart"; import "package:json_schema/json_schema.dart"; import "../definitions/data_schema.dart"; -import "../exceptions.dart"; import "../scripting_api/data_schema_value.dart"; import "codecs/cbor_codec.dart"; import "codecs/codec_media_type.dart"; @@ -145,7 +144,7 @@ class ContentSerdes { } if (dataSchemaValue == null) { - throw const ValidationException("Expected a defined dataSchemaValue"); + throw const FormatException("Expected a defined dataSchemaValue"); } final schema = JsonSchema.create( @@ -153,7 +152,7 @@ class ContentSerdes { schemaVersion: SchemaVersion.draft7, ); if (!schema.validate(dataSchemaValue.value).isValid) { - throw const ValidationException("JSON Schema validation failed."); + throw const FormatException("JSON Schema validation failed."); } } diff --git a/lib/src/core/implementation/exposed_thing.dart b/lib/src/core/implementation/exposed_thing.dart index 2e1d3a57..1a2e6635 100644 --- a/lib/src/core/implementation/exposed_thing.dart +++ b/lib/src/core/implementation/exposed_thing.dart @@ -13,8 +13,7 @@ import "servient.dart"; class ExposedThing implements scripting_api.ExposedThing { /// Creates a new [ExposedThing] from a [servient] and an [exposedThingInit]. ExposedThing(this.servient, scripting_api.ExposedThingInit exposedThingInit) - : thingDescription = - ThingDescription.fromJson(exposedThingInit, validate: false); + : thingDescription = ThingDescription.fromJson(exposedThingInit); @override final ThingDescription thingDescription; diff --git a/lib/src/core/implementation/servient.dart b/lib/src/core/implementation/servient.dart index ab7fd9df..8351042c 100644 --- a/lib/src/core/implementation/servient.dart +++ b/lib/src/core/implementation/servient.dart @@ -249,22 +249,98 @@ class InternalServient implements Servient { Future produce( scripting_api.ExposedThingInit init, ) async { - const uuid = Uuid(); + final thingDescription = _expandExposedThingInit(init); - final exposedThingInit = { - "id": "urn:uuid:${uuid.v4()}", - ...init, - }; - - final newThing = ExposedThing(this, exposedThingInit); + final newThing = ExposedThing(this, thingDescription); if (addThing(newThing)) { return newThing; + } + + final id = newThing.thingDescription.identifier; + throw DartWotException( + "An ExposedThing with identifier $id already exists.", + ); + } + + List _determineUnsupportedSecuritySchemes( + scripting_api.ExposedThingInit exposedThingInit, + ) { + // TODO: Implement + return []; + } + + // TODO: Implement missing steps of algorithm, + // see https://w3c.github.io/wot-scripting-api/#expand-an-exposedthinginit + Map _expandExposedThingInit( + scripting_api.ExposedThingInit exposedThingInit, + ) { + final thingDescription = Map.of(exposedThingInit); + + if (!thingDescription.containsKey("@context")) { + thingDescription["@context"] = "https://www.w3.org/2022/wot/td/v1.1"; + } + + final unsupportedSecuritySchemes = + _determineUnsupportedSecuritySchemes(thingDescription); + + final securityDefinitions = thingDescription["securityDefinitions"] ?? + >{}; + + if (securityDefinitions is! Map>) { + throw FormatException( + "Incorrect type for securitySchemes field, " + "expected Map>, got " + "${securityDefinitions.runtimeType}", + ); + } + + unsupportedSecuritySchemes.forEach(securityDefinitions.remove); + + final security = []; + + // TODO: Refactor + final tdSecurity = thingDescription["security"] ?? []; + + if (tdSecurity is List) { + security.addAll(tdSecurity); + } else if (tdSecurity is String) { + security.add(tdSecurity); } else { - final id = newThing.thingDescription.identifier; - throw DartWotException( - "A ConsumedThing with identifier $id already exists.", + throw FormatException( + "Incorrect type for security field, " + "expected List, got " + "${tdSecurity.runtimeType}", ); } + + thingDescription["security"] = + security.where(securityDefinitions.containsKey).toList(); + + // TODO: Should an ID be assigned here? + if (!thingDescription.containsKey("id")) { + const uuid = Uuid(); + + thingDescription["id"] = "urn:uuid:${uuid.v4()}"; + } + + if (!thingDescription.containsKey("title")) { + // TODO: Decide which title should be used here. + thingDescription["title"] = "Exposed Thing"; + } + + if (security.isEmpty) { + if (securityDefinitions.isEmpty) { + securityDefinitions["nosec_sc"] = { + "scheme": "nosec", + }; + thingDescription["securityDefinitions"] = securityDefinitions; + } + + thingDescription["security"] = + securityDefinitions.keys.firstOrNull ?? "nosec_sc"; + } + + return thingDescription; } /// Perform automatic discovery using this [InternalServient]'s diff --git a/lib/src/core/implementation/thing_discovery.dart b/lib/src/core/implementation/thing_discovery.dart index 5c916daf..28e40776 100644 --- a/lib/src/core/implementation/thing_discovery.dart +++ b/lib/src/core/implementation/thing_discovery.dart @@ -409,7 +409,7 @@ class ThingDiscovery extends Stream return dataSchemaValue.value.toThingDescription(); } - throw ValidationException( + throw FormatException( "Encountered wrong datatype ${dataSchemaValue.runtimeType} that cannot " "be processed as a Thing Description.", ); diff --git a/lib/src/core/scripting_api/exposed_thing.dart b/lib/src/core/scripting_api/exposed_thing.dart index ee873b98..cf6048ac 100644 --- a/lib/src/core/scripting_api/exposed_thing.dart +++ b/lib/src/core/scripting_api/exposed_thing.dart @@ -104,7 +104,7 @@ abstract interface class ExposedThing { /// Assigns a [handler] function to an event with a given [name]. /// - /// If the event is ubsubscribed, the [handler] function will be called + /// If the event is unsubscribed, the [handler] function will be called /// to handle the interaction. void setEventUnsubscribeHandler( String name, @@ -117,7 +117,7 @@ abstract interface class ExposedThing { void setEventHandler(String name, EventListenerHandler handler); /// Informs all subscribers of an Event with the given [name] that it has - /// occured. + /// occurred. /// /// You can provide (optional) input [data] that is emitted with the event. Future emitEvent(String name, InteractionInput data); diff --git a/test/binding_coap/coap_definitions_test.dart b/test/binding_coap/coap_definitions_test.dart index d627a019..f525bb12 100644 --- a/test/binding_coap/coap_definitions_test.dart +++ b/test/binding_coap/coap_definitions_test.dart @@ -80,11 +80,11 @@ void main() { ); expect( () => invalidForm.block1Size, - throwsA(isA()), + throwsA(isA()), ); expect( () => invalidForm.block2Size, - throwsA(isA()), + throwsA(isA()), ); }); }); diff --git a/test/binding_mqtt/mqtt_extension_test.dart b/test/binding_mqtt/mqtt_extension_test.dart index 99fa4a2f..1afb4e3c 100644 --- a/test/binding_mqtt/mqtt_extension_test.dart +++ b/test/binding_mqtt/mqtt_extension_test.dart @@ -52,7 +52,7 @@ void main() { expect( () => augmentedForm.qualityOfService, - throwsA(isA()), + throwsA(isA()), ); }); }); diff --git a/test/core/augmented_form_test.dart b/test/core/augmented_form_test.dart index 9e38e9f2..caaae7bb 100644 --- a/test/core/augmented_form_test.dart +++ b/test/core/augmented_form_test.dart @@ -194,7 +194,7 @@ void main() { expect( () => augmentedForm4.resolvedHref, - throwsA(isA()), + throwsA(isA()), ); final augmentedForm5 = AugmentedForm( @@ -209,7 +209,7 @@ void main() { expect( () => augmentedForm5.resolvedHref, - throwsA(isA()), + throwsA(isA()), ); final augmentedForm6 = AugmentedForm( diff --git a/test/core/consumed_thing_test.dart b/test/core/consumed_thing_test.dart index 5e00de3d..29830b3c 100644 --- a/test/core/consumed_thing_test.dart +++ b/test/core/consumed_thing_test.dart @@ -258,7 +258,7 @@ void main() { "status2", uriVariables: uriVariables, ), - throwsA(const TypeMatcher()), + throwsA(const TypeMatcher()), ); await servient.shutdown(); diff --git a/test/core/content_serdes_test.dart b/test/core/content_serdes_test.dart index 997f6e3e..a4de1102 100644 --- a/test/core/content_serdes_test.dart +++ b/test/core/content_serdes_test.dart @@ -6,7 +6,6 @@ import "package:curie/curie.dart"; import "package:dart_wot/src/core/definitions/data_schema.dart"; -import "package:dart_wot/src/core/exceptions.dart"; import "package:dart_wot/src/core/implementation/codecs/json_codec.dart"; import "package:dart_wot/src/core/implementation/content.dart"; import "package:dart_wot/src/core/implementation/content_serdes.dart"; @@ -41,7 +40,7 @@ void main() { expect( contentSerdes.contentToValue(testContent2, failingSchema), - throwsA(const TypeMatcher()), + throwsA(const TypeMatcher()), ); expect( @@ -49,7 +48,7 @@ void main() { DataSchemaValue.tryParse(42), failingSchema, ), - throwsA(const TypeMatcher()), + throwsA(const TypeMatcher()), ); final testContent3 = _getTestContent(""); @@ -150,7 +149,7 @@ void main() { // FIXME(JKRhb): Should not be necessary to use fromJson here DataSchema.fromJson(const {"type": "object"}, PrefixMapping()), ), - throwsA(isA()), + throwsA(isA()), ); }); diff --git a/test/core/context_test.dart b/test/core/context_test.dart index f6f1bfab..ecf8f4aa 100644 --- a/test/core/context_test.dart +++ b/test/core/context_test.dart @@ -4,7 +4,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/definitions/extensions/json_parser.dart"; import "package:test/test.dart"; @@ -17,7 +16,7 @@ void main() { expect( () => Context([illegalSingleContextEntry]), - throwsA(isA()), + throwsA(isA()), ); }); @@ -90,7 +89,7 @@ void main() { test("only be valid when created from a valid URI", () { expect( () => SingleContextEntry.fromString("::foobar::"), - throwsA(isA()), + throwsA(isA()), ); }); diff --git a/test/core/definitions_test.dart b/test/core/definitions_test.dart index fa125791..3327cec6 100644 --- a/test/core/definitions_test.dart +++ b/test/core/definitions_test.dart @@ -23,7 +23,7 @@ void main() { expect( () => ThingDescription.fromJson(illegalThingDescription), - throwsA(isA()), + throwsA(isA()), ); }); @@ -220,7 +220,7 @@ void main() { PrefixMapping(), {}, ), - throwsA(isA()), + throwsA(isA()), ); }); @@ -481,7 +481,7 @@ void main() { expect( () => OperationType.fromString("test"), - throwsA(isA()), + throwsA(isA()), ); }); @@ -516,11 +516,8 @@ void main() { }; expect( - () => ThingDescription.fromJson( - invalidThingDescription1, - validate: false, - ), - throwsA(isA()), + () => ThingDescription.fromJson(invalidThingDescription1), + throwsA(isA()), ); final invalidThingDescription2 = { @@ -534,11 +531,11 @@ void main() { expect( () => ThingDescription.fromJson(invalidThingDescription2), - throwsA(isA()), + throwsA(isA()), ); }); - test("Should reject invalid @context entries", () { + test("Should reject invalid @context entry types", () { // TODO(JKRhb): Double-check if this the correct behavior. final invalidThingDescription1 = { "@context": [ @@ -556,7 +553,129 @@ void main() { expect( () => ThingDescription.fromJson(invalidThingDescription1), - throwsA(isA()), + throwsA(isA()), + ); + }); + + test("Should throw FormatExceptions for incorrect array value types in TDs", + () { + final invalidThingDescription = { + "@context": ["https://www.w3.org/2022/wot/td/v1.1"], + "title": "Thingweb WoT Thing", + "security": [2], + "securityDefinitions": { + "nosec_sc": { + "scheme": "nosec", + }, + }, + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription), + throwsA(isA()), + ); + }); + + test("Should throw FormatExceptions for incorrect Map value types in TDs", + () { + final invalidThingDescription = { + "@context": ["https://www.w3.org/2022/wot/td/v1.1"], + "title": "Thingweb WoT Thing", + "titles": { + "de": 42, + }, + "security": ["nosec_sc"], + "securityDefinitions": { + "nosec_sc": { + "scheme": "nosec", + }, + }, + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription), + throwsA(isA()), + ); + }); + + test("Should throw FormatExceptions for incorrect field types in TDs", () { + final invalidThingDescription = { + "@context": ["https://www.w3.org/2022/wot/td/v1.1"], + "title": 42, + "security": ["nosec_sc"], + "securityDefinitions": { + "nosec_sc": { + "scheme": "nosec", + }, + }, + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription), + throwsA(isA()), + ); + }); + + test("Should throw FormatExceptions for incorrect DataSchema types in TDs", + () { + final invalidThingDescription = { + "@context": ["https://www.w3.org/2022/wot/td/v1.1"], + "title": "Thingweb WoT Thing", + "security": ["nosec_sc"], + "securityDefinitions": { + "nosec_sc": { + "scheme": "nosec", + }, + }, + "schemaDefinitions": { + "invalidSchema": 42, + }, + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription), + throwsA(isA()), + ); + }); + + test("Should throw FormatExceptions for empty affordance forms in TDs", () { + final invalidThingDescription = { + "@context": ["https://www.w3.org/2022/wot/td/v1.1"], + "title": "Thingweb WoT Thing", + "security": ["nosec_sc"], + "securityDefinitions": { + "nosec_sc": { + "scheme": "nosec", + }, + }, + "actions": { + "testAction": { + "forms": [], + }, + }, + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription), + throwsA(isA()), + ); + }); + + test("Should throw FormatExceptions for wrong array field types", () { + final invalidThingDescription = { + "@context": ["https://www.w3.org/2022/wot/td/v1.1"], + "title": "Thingweb WoT Thing", + "security": 42, + "securityDefinitions": { + "nosec_sc": { + "scheme": "nosec", + }, + }, + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription), + throwsA(isA()), ); }); } diff --git a/test/core/exceptions_test.dart b/test/core/exceptions_test.dart index 81098ec0..7c277261 100644 --- a/test/core/exceptions_test.dart +++ b/test/core/exceptions_test.dart @@ -9,20 +9,15 @@ import "package:test/test.dart"; void main() { group("DartWotException should", () { - test("be indicate the respective name in its toString() method", () { + test("indicate the respective name in its toString() method", () { expect( const DartWotException("test").toString(), "DartWotException: test", ); expect( - const ValidationException("test").toString(), - "ValidationException: test", - ); - - expect( - const ValidationException("test", ["test", "test"]).toString(), - "ValidationException: test\n\nErrors:\n\ntest\ntest", + const FormatException("test").toString(), + "FormatException: test", ); expect( diff --git a/test/core/servient_test.dart b/test/core/servient_test.dart index 9c588b96..5d01d403 100644 --- a/test/core/servient_test.dart +++ b/test/core/servient_test.dart @@ -5,6 +5,7 @@ // 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"; @@ -75,4 +76,98 @@ void main() { ); }, ); + + test( + "should throw a FormatException when trying to expand invalid " + "ExposedThingInits", + () { + final servient = InternalServient(); + + final exposedThingInit1 = { + "securityDefinitions": 42, + }; + + expect( + () async => await servient.produce(exposedThingInit1), + throwsA(isA()), + ); + + final exposedThingInit2 = { + "security": 42, + }; + + expect( + () async => await servient.produce(exposedThingInit2), + throwsA(isA()), + ); + }, + ); + + test( + "should fill in missing values during the expansion of ExposedThingInits", + () async { + final servient = InternalServient(); + + final exposedThingInit = {}; + + final exposedThing = await servient.produce(exposedThingInit); + + expect( + exposedThing.thingDescription.security, + ["nosec_sc"], + ); + + final securityDefinitions = + exposedThing.thingDescription.securityDefinitions; + + expect(securityDefinitions.length, 1); + expect(securityDefinitions["nosec_sc"], isA()); + + expect(exposedThing.thingDescription.title, "Exposed Thing"); + expect(exposedThing.thingDescription.id, matches("urn:uuid:")); + + final context = exposedThing.thingDescription.context; + expect(context.contextEntries.length, 1); + expect( + context.contextEntries[0], + SingleContextEntry(Uri.parse("https://www.w3.org/2022/wot/td/v1.1")), + ); + }, + ); + + test( + "should support ExposedThingInits with both single values and arrays for " + "security", + () async { + final servient = InternalServient(); + + final exposedThingInits = [ + { + "security": "basic_sc", + "securityDefinitions": { + "basic_sc": { + "scheme": "basic", + }, + }, + }, + { + "security": ["basic_sc"], + "securityDefinitions": { + "basic_sc": { + "scheme": "basic", + }, + }, + }, + ]; + + for (final exposedThingInit in exposedThingInits) { + final exposedThing = await servient.produce(exposedThingInit); + + expect( + exposedThing.thingDescription.security, + ["basic_sc"], + ); + } + }, + ); } diff --git a/test/core/thing_description_test.dart b/test/core/thing_description_test.dart index aa5d7ca0..20c72926 100644 --- a/test/core/thing_description_test.dart +++ b/test/core/thing_description_test.dart @@ -39,7 +39,7 @@ void main() { expect(thingDescriptionJson, thingDescription.toJson()); }); - test("throw a ValidationException when it is invalid during parsing", () { + test("throw a FormatException when it is invalid during parsing", () { const thingDescriptionJson = { "@context": [ "https://www.w3.org/2022/wot/td/v1.1", @@ -52,7 +52,7 @@ void main() { expect( () => ThingDescription.fromJson(thingDescriptionJson), - throwsA(isA()), + throwsA(isA()), ); }); @@ -73,5 +73,95 @@ void main() { "https://www.w3.org/2022/wot/td/v1.1", ); }); + + test("reject invalid ComboSecuritySchemes", () { + final invalidThingDescription1 = { + "@context": [ + "https://www.w3.org/2022/wot/td/v1.1", + ], + "title": "Invalid TD with missing security field.", + "security": "combo_sc", + "securityDefinitions": { + "combo_sc": {"scheme": "combo"}, + }, + }; + + expect( + invalidThingDescription1.toThingDescription, + throwsFormatException, + ); + + final invalidThingDescription2 = { + "@context": [ + "https://www.w3.org/2022/wot/td/v1.1", + ], + "title": "Invalid TD with missing security field.", + "security": "combo_sc", + "securityDefinitions": { + "nosec_sc1": {"scheme": "nosec"}, + "nosec_sc2": {"scheme": "nosec"}, + "combo_sc": { + "scheme": "combo", + "oneOf": [ + "nosec_sc1", + "nosec_sc2", + ], + "allOf": [ + "nosec_sc1", + "nosec_sc2", + ], + }, + }, + }; + + expect( + invalidThingDescription2.toThingDescription, + throwsFormatException, + ); + + final invalidThingDescription3 = { + "@context": [ + "https://www.w3.org/2022/wot/td/v1.1", + ], + "title": "Invalid TD with missing security field.", + "security": "combo_sc", + "securityDefinitions": { + "nosec_sc1": {"scheme": "nosec"}, + "combo_sc": { + "scheme": "combo", + "oneOf": [ + "nosec_sc1", + ], + }, + }, + }; + + expect( + invalidThingDescription3.toThingDescription, + throwsFormatException, + ); + + final invalidThingDescription4 = { + "@context": [ + "https://www.w3.org/2022/wot/td/v1.1", + ], + "title": "Invalid TD with missing security field.", + "security": "combo_sc", + "securityDefinitions": { + "nosec_sc1": {"scheme": "nosec"}, + "combo_sc": { + "scheme": "combo", + "allOf": [ + "nosec_sc1", + ], + }, + }, + }; + + expect( + invalidThingDescription4.toThingDescription, + throwsFormatException, + ); + }); }); }