diff --git a/lib/src/core/definitions/data_schema.dart b/lib/src/core/definitions/data_schema.dart index 22731bff..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); diff --git a/lib/src/core/definitions/extensions/json_parser.dart b/lib/src/core/definitions/extensions/json_parser.dart index cb9e38ca..4222cd19 100644 --- a/lib/src/core/definitions/extensions/json_parser.dart +++ b/lib/src/core/definitions/extensions/json_parser.dart @@ -90,8 +90,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; @@ -182,30 +190,46 @@ extension ParseField on Map { /// will be added. This can be used for filtering when parsing additional /// fields. List? parseArrayField( - String name, [ + String name, { Set? parsedFields, - ]) { + int minimalSize = 0, + }) { final fieldValue = parseField(name, parsedFields); if (fieldValue == null) { return null; } - if (fieldValue is T) { - return [fieldValue]; - } else if (fieldValue is List) { - return fieldValue; + final List result; + + if (fieldValue is List) { + result = fieldValue; + } else if (fieldValue is T) { + result = [fieldValue]; } else if (fieldValue is List) { final filteredArray = fieldValue.whereType().toList(growable: false); if (filteredArray.length == fieldValue.length) { - return filteredArray; + 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}", + ); } - throw FormatException( - "Expected $T or a List of $T, got ${fieldValue.runtimeType}", - ); + 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 @@ -218,10 +242,15 @@ extension ParseField on Map { /// will be added. This can be used for filtering when parsing additional /// fields. List parseRequiredArrayField( - String name, [ + String name, { Set? parsedFields, - ]) { - final result = parseArrayField(name, parsedFields); + int minimalSize = 0, + }) { + final result = parseArrayField( + name, + parsedFields: parsedFields, + minimalSize: minimalSize, + ); if (result == null) { throw FormatException("Missing required field $name"); @@ -313,12 +342,14 @@ extension ParseField on Map { PrefixMapping prefixMapping, Set? parsedFields, ) { - final fieldValue = - parseField>>("forms", parsedFields); + final fieldValue = parseArrayField>( + "forms", + parsedFields: parsedFields, + minimalSize: 1, + ); return fieldValue - ?.whereType>() - .map( + ?.map( (e) => Form.fromJson( e, prefixMapping, @@ -340,13 +371,13 @@ extension ParseField on Map { parsedFields, ); - if (forms != null) { - return forms; + if (forms == null) { + throw const FormatException( + 'Missing "forms" member in InteractionAffordance', + ); } - throw const FormatException( - 'Missing "forms" member in InteractionAffordance', - ); + return forms; } /// Parses [Link]s contained in this JSON object. @@ -502,9 +533,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(); } @@ -532,11 +568,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) { 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/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/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 620601ed..1ba82fbc 100644 --- a/lib/src/core/definitions/thing_description.dart +++ b/lib/src/core/definitions/thing_description.dart @@ -58,7 +58,8 @@ class ThingDescription { 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); @@ -71,13 +72,22 @@ class ThingDescription { final base = json.parseUriField("base", parsedFields); final id = json.parseField("id", parsedFields); - final security = - json.parseRequiredArrayField("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); @@ -85,7 +95,11 @@ 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, 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/test/core/definitions_test.dart b/test/core/definitions_test.dart index 7e6f44b5..e1db1f00 100644 --- a/test/core/definitions_test.dart +++ b/test/core/definitions_test.dart @@ -638,30 +638,26 @@ void main() { ); }); - 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", - }, + 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": [], - }, + }, + "actions": { + "testAction": { + "forms": [], }, - }; + }, + }; - expect( - () => ThingDescription.fromJson(invalidThingDescription), - throwsA(isA()), - ); - }, - skip: true, - ); + expect( + () => ThingDescription.fromJson(invalidThingDescription), + throwsA(isA()), + ); + }); }