From 506d20b9f5562359137d9325f66e12c85aaa66a0 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 22 Sep 2022 00:20:03 +0200 Subject: [PATCH] refactor: rework data schema implementation --- .../additional_expected_response.dart | 64 +++---- lib/src/definitions/context_entry.dart | 81 ++++++++ lib/src/definitions/data_schema.dart | 35 ++-- lib/src/definitions/expected_response.dart | 40 ++-- .../definitions/extensions/json_parser.dart | 87 +++++++++ lib/src/definitions/form.dart | 100 ++-------- .../interaction_affordance.dart | 44 +---- .../interaction_affordances/property.dart | 15 +- lib/src/definitions/link.dart | 66 ++----- lib/src/definitions/operation_type.dart | 18 +- .../security/ace_security_scheme.dart | 46 +---- .../security/apikey_security_scheme.dart | 42 ++-- .../security/auto_security_scheme.dart | 6 +- .../security/basic_security_scheme.dart | 42 ++-- .../security/bearer_security_scheme.dart | 78 +++----- .../security/digest_security_scheme.dart | 69 ++----- .../security/helper_functions.dart | 56 ------ .../security/no_security_scheme.dart | 6 +- .../security/oauth2_security_scheme.dart | 61 ++---- .../security/psk_security_scheme.dart | 19 +- .../definitions/security/security_scheme.dart | 66 ++++++- lib/src/definitions/thing_description.dart | 179 ++---------------- test/core/dart_wot_test.dart | 3 +- test/core/definitions_test.dart | 130 ++++++++++++- 24 files changed, 596 insertions(+), 757 deletions(-) create mode 100644 lib/src/definitions/extensions/json_parser.dart delete mode 100644 lib/src/definitions/security/helper_functions.dart diff --git a/lib/src/definitions/additional_expected_response.dart b/lib/src/definitions/additional_expected_response.dart index 77689312..3dbede58 100644 --- a/lib/src/definitions/additional_expected_response.dart +++ b/lib/src/definitions/additional_expected_response.dart @@ -7,6 +7,8 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; +import 'extensions/json_parser.dart'; + /// Communication metadata describing the expected response message for the /// primary response. @immutable @@ -14,59 +16,53 @@ class AdditionalExpectedResponse { /// Constructs a new [AdditionalExpectedResponse] object from a [contentType]. AdditionalExpectedResponse( this.contentType, { - String? schema, + this.schema, bool? success, - }) : _success = success, - _schema = schema; + Map? additionalFields, + }) : additionalFields = additionalFields ?? {}, + success = success ?? false; /// Creates an [AdditionalExpectedResponse] from a [json] object. - AdditionalExpectedResponse.fromJson( + factory AdditionalExpectedResponse.fromJson( Map json, String formContentType, - ) : contentType = _parseJson(json, 'contentType') ?? formContentType, - _success = _parseJson(json, 'success'), - _schema = _parseJson(json, 'schema') { - const parsedFields = ['contentType', 'schema', 'success']; - - for (final entry in json.entries) { - final key = entry.key; - if (parsedFields.contains(key)) { - continue; - } - - additionalFields[key] = entry.value; - } - } - - static T? _parseJson(Map json, String key) { - final dynamic value = json[key]; - - if (value is T) { - return value; - } - - return null; + ) { + final Set parsedFields = {}; + + final contentType = + json.parseField('contentType', parsedFields) ?? formContentType; + final success = json.parseField('success', parsedFields); + final schema = json.parseField('schema', parsedFields); + + final additionalFields = Map.fromEntries( + json.entries.where((entry) => !parsedFields.contains(entry.key)), + ); + + return AdditionalExpectedResponse( + contentType, + schema: schema, + success: success, + additionalFields: additionalFields, + ); } /// The [contentType] of this [AdditionalExpectedResponse] object. final String contentType; - final bool? _success; - /// Signals if an additional response should not be considered an error. - bool get success => _success ?? false; - - final String? _schema; + /// + /// Defaults to `false` if not explicitly set. + final bool success; /// Used to define the output data schema for an additional response if it /// differs from the default output data schema. /// /// Rather than a `DataSchema` object, the name of a previous definition given /// in a `schemaDefinitions` map must be used. - String? get schema => _schema; + final String? schema; /// Any other additional field will be included in this [Map]. - final Map additionalFields = {}; + final Map additionalFields; @override bool operator ==(Object other) { diff --git a/lib/src/definitions/context_entry.dart b/lib/src/definitions/context_entry.dart index 5d7028a8..24615e28 100644 --- a/lib/src/definitions/context_entry.dart +++ b/lib/src/definitions/context_entry.dart @@ -4,8 +4,17 @@ // // SPDX-License-Identifier: BSD-3-Clause +import 'package:curie/curie.dart'; import 'package:meta/meta.dart'; +import 'validation/validation_exception.dart'; + +const _validTdContextValues = [ + 'https://www.w3.org/2019/wot/td/v1', + 'https://www.w3.org/2022/wot/td/v1.1', + 'http://www.w3.org/ns/td' +]; + /// Class holding a [value] and an optional [key] for representing different /// types of `@context` entries. @immutable @@ -13,6 +22,78 @@ class ContextEntry { /// Creates a new [ContextEntry]. const ContextEntry(this.value, this.key); + /// Parses a single `@context` entry from a given [json] value. + /// + /// @context extensions are added to the provided [prefixMapping]. + /// If the given entry is the [firstEntry], it will be set in the + /// [prefixMapping] accordingly. + factory ContextEntry.fromJson( + dynamic json, + PrefixMapping prefixMapping, { + required bool firstEntry, + }) { + if (json is String) { + if (firstEntry && _validTdContextValues.contains(json)) { + prefixMapping.defaultPrefixValue = json; + return ContextEntry(json, null); + } + } + + if (json is Map) { + for (final contextEntry in json.entries) { + final key = contextEntry.key; + final value = contextEntry.value; + if (value is String) { + if (!key.startsWith('@') && Uri.tryParse(value) != null) { + prefixMapping.addPrefix(key, value); + } + return ContextEntry(value, key); + } + } + } + + throw ValidationException( + 'Excepted either a String or a Map ' + 'as @context entry, got ${json.runtimeType} instead.', + ); + } + + /// Parses a TD `@context` from a [json] value. + /// + /// @context extensions are added to the provided [prefixMapping]. + static List parseContext( + dynamic json, + PrefixMapping prefixMapping, + ) { + var firstEntry = true; + + if (json is String) { + return [ + ContextEntry.fromJson(json, prefixMapping, firstEntry: firstEntry) + ]; + } + + if (json is List) { + final List result = []; + for (final contextEntry in json) { + result.add( + ContextEntry.fromJson( + contextEntry, + prefixMapping, + firstEntry: firstEntry, + ), + ); + firstEntry = false; + } + return result; + } + + throw ValidationException( + 'Excepted either a single @context entry or a List of @context entries, ' + 'got ${json.runtimeType} instead.', + ); + } + /// The [value] of this [ContextEntry]. final String value; diff --git a/lib/src/definitions/data_schema.dart b/lib/src/definitions/data_schema.dart index bfae10df..cdfcce8a 100644 --- a/lib/src/definitions/data_schema.dart +++ b/lib/src/definitions/data_schema.dart @@ -4,30 +4,21 @@ // // SPDX-License-Identifier: BSD-3-Clause +import 'extensions/json_parser.dart'; + /// Parses a [json] object and adds its contents to a [dataSchema]. void parseDataSchemaJson(DataSchema dataSchema, Map json) { - // TODO(JKRhb): Parse more DataSchema values - final Object? atType = json['@type']; - if (atType is String) { - dataSchema.atType = [atType]; - } else if (atType is List) { - dataSchema.atType = atType; - } - - final Object? type = json['type']; - if (type is String) { - dataSchema.type = type; - } - - final Object? readOnly = json['readOnly']; - if (readOnly is bool) { - dataSchema.readOnly = readOnly; - } - - final Object? writeOnly = json['writeOnly']; - if (writeOnly is bool) { - dataSchema.writeOnly = writeOnly; - } + dataSchema + ..atType = json.parseArrayField('@type') + ..title = json.parseField('title') + ..titles = json.parseMapField('titles') + ..description = json.parseField('description') + ..constant = json.parseField('constant') + ..enumeration = json.parseField>('enum') + ..readOnly = json.parseField('readOnly') ?? dataSchema.readOnly + ..writeOnly = json.parseField('writeOnly') ?? dataSchema.writeOnly + ..format = json.parseField('format') + ..type = json.parseField('type'); } /// Metadata that describes the data format used. It can be used for validation. diff --git a/lib/src/definitions/expected_response.dart b/lib/src/definitions/expected_response.dart index cc725f05..6934506e 100644 --- a/lib/src/definitions/expected_response.dart +++ b/lib/src/definitions/expected_response.dart @@ -4,36 +4,42 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'validation/validation_exception.dart'; +import 'extensions/json_parser.dart'; /// Communication metadata describing the expected response message for the /// primary response. class ExpectedResponse { /// Constructs a new [ExpectedResponse] object from a [contentType]. - ExpectedResponse(this.contentType); + ExpectedResponse(this.contentType, {Map? additionalFields}) + : additionalFields = Map.fromEntries( + additionalFields?.entries + .where((element) => element.key != 'contentType') ?? + [], + ); /// Creates an [ExpectedResponse] from a [json] object. - ExpectedResponse.fromJson(Map json) - : contentType = _parseContentType(json['contentType']) { - for (final entry in json.entries) { - if (entry.key == 'response') { - continue; - } + static ExpectedResponse? fromJson( + Map json, [ + Set? parsedFields, + ]) { + final responseJson = json['response']; + parsedFields?.add('response'); - additionalFields[entry.key] = entry.value; + if (responseJson is! Map) { + return null; } + + return ExpectedResponse( + responseJson.parseRequiredField('contentType'), + additionalFields: Map.fromEntries( + responseJson.entries.where((element) => element.key != 'contentType'), + ), + ); } /// The [contentType] of this [ExpectedResponse] object. String contentType; /// Any other additional field will be included in this [Map]. - final Map additionalFields = {}; - - static String _parseContentType(dynamic contentType) { - if (contentType is! String) { - throw ValidationException('contentType of response map is not a String!'); - } - return contentType; - } + final Map additionalFields; } diff --git a/lib/src/definitions/extensions/json_parser.dart b/lib/src/definitions/extensions/json_parser.dart new file mode 100644 index 00000000..619603d9 --- /dev/null +++ b/lib/src/definitions/extensions/json_parser.dart @@ -0,0 +1,87 @@ +import '../validation/validation_exception.dart'; + +/// Extension for parsing fields of JSON objects. +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 a [Set] of [parsedFields] is passed to this function, the field [name] + /// will added. This can be used for filtering when parsing additional fields. + T? parseField(String name, [Set? parsedFields]) { + final value = this[name]; + parsedFields?.add(name); + if (value is T) { + return value; + } + + return null; + } + + /// 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]. + /// + /// Like [parseField], it adds the field [name] to the set of [parsedFields], + /// if present. + T parseRequiredField(String name, [Set? parsedFields]) { + final value = parseField(name, parsedFields); + + if (value is! T) { + throw ValidationException( + 'Value for field $name has wrong data type or is missing. ' + 'Expected ${T.runtimeType}, got ${value.runtimeType}.', + ); + } + + return value; + } + + /// Parses a map 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 a [Set] of [parsedFields] is passed to this function, the field [name] + /// will added. This can be used for filtering when parsing additional fields. + Map? parseMapField(String name, [Set? parsedFields]) { + final dynamic mapField = this[name]; + parsedFields?.add(name); + + if (mapField is Map) { + final Map result = {}; + + for (final entry in mapField.entries) { + final value = entry.value; + if (value is T) { + result[entry.key] = value; + } + } + + return result; + } + return null; + } + + /// 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. + /// + /// 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]) { + final value = this[name]; + parsedFields?.add(name); + + if (value is T) { + return [value]; + } else if (value is List) { + return value.whereType().toList(growable: false); + } + + return null; + } +} diff --git a/lib/src/definitions/form.dart b/lib/src/definitions/form.dart index b946f80e..6a019651 100644 --- a/lib/src/definitions/form.dart +++ b/lib/src/definitions/form.dart @@ -10,6 +10,7 @@ import 'package:uri/uri.dart'; import 'additional_expected_response.dart'; import 'expected_response.dart'; +import 'extensions/json_parser.dart'; import 'interaction_affordances/action.dart'; import 'interaction_affordances/event.dart'; import 'interaction_affordances/interaction_affordance.dart'; @@ -49,78 +50,28 @@ class Form { Map json, InteractionAffordance interactionAffordance, ) { - final List parsedJsonFields = []; - final href = _parseHref(json, parsedJsonFields); + final Set parsedFields = {}; + final href = + Uri.parse(json.parseRequiredField('href', parsedFields)); - String? subprotocol; - if (json['subprotocol'] is String) { - parsedJsonFields.add('subprotocol'); - subprotocol = json['subprotocol'] as String; - } - - List? op; - if (json['op'] != null) { - final dynamic jsonOp = _getJsonValue(json, 'op', parsedJsonFields); - if (jsonOp is String) { - op = [jsonOp]; - } else if (jsonOp is List) { - op = jsonOp.whereType().toList(growable: false); - } - } + final subprotocol = json.parseField('subprotocol', parsedFields); - String contentType = 'application/json'; - if (json['contentType'] != null) { - final dynamic jsonContentType = - _getJsonValue(json, 'contentType', parsedJsonFields); - if (jsonContentType is String) { - contentType = jsonContentType; - } - } + final List? op = json.parseArrayField('op', parsedFields); - String? contentCoding; - if (json['contentCoding'] != null) { - final dynamic jsonContentCoding = - _getJsonValue(json, 'contentCoding', parsedJsonFields); - if (jsonContentCoding is String) { - contentCoding = jsonContentCoding; - } - } + final contentType = json.parseField('contentType', parsedFields) ?? + 'application/json'; - List? security; - if (json['security'] != null) { - final dynamic jsonSecurity = - _getJsonValue(json, 'security', parsedJsonFields); - if (jsonSecurity is String) { - security = [jsonSecurity]; - } else if (jsonSecurity is List) { - security = jsonSecurity.whereType().toList(growable: false); - } - } + final contentCoding = + json.parseField('contentCoding', parsedFields); - List? scopes; - if (json['scopes'] != null) { - final dynamic jsonScopes = - _getJsonValue(json, 'scopes', parsedJsonFields); - if (jsonScopes is String) { - scopes = [jsonScopes]; - } else if (jsonScopes is List) { - scopes = jsonScopes.whereType().toList(growable: false); - } - } - - ExpectedResponse? response; - if (json['response'] != null) { - final dynamic jsonResponse = - _getJsonValue(json, 'response', parsedJsonFields); - if (jsonResponse is Map) { - response = ExpectedResponse.fromJson(jsonResponse); - } - } + final security = json.parseArrayField('security', parsedFields); + final scopes = json.parseArrayField('scopes', parsedFields); + final response = ExpectedResponse.fromJson(json, parsedFields); List? additionalResponses; if (json['additionalResponses'] != null) { final dynamic jsonResponse = - _getJsonValue(json, 'additionalResponses', parsedJsonFields); + _getJsonValue(json, 'additionalResponses', parsedFields); if (jsonResponse is Map) { additionalResponses = [ AdditionalExpectedResponse.fromJson(jsonResponse, contentType) @@ -138,7 +89,7 @@ class Form { final additionalFields = _parseAdditionalFields( json, - parsedJsonFields, + parsedFields, interactionAffordance.thingDescription.prefixMapping, ); @@ -252,19 +203,6 @@ class Form { } } - static Uri _parseHref( - Map json, - List parsedJsonFields, - ) { - final dynamic href = json['href']; - parsedJsonFields.add('href'); - if (href is String) { - return Uri.parse(href); - } else { - throw ValidationException("'href' field must be a string."); - } - } - static List _setOpValue( InteractionAffordance interactionAffordance, List? opStrings, @@ -290,14 +228,14 @@ class Form { throw StateError( 'Encountered unknown InteractionAffordance ' - '${interactionAffordance.runtimeType} encountered', + '${interactionAffordance.runtimeType}.', ); } static dynamic _getJsonValue( Map formJson, String key, - List parsedJsonFields, + Set parsedJsonFields, ) { parsedJsonFields.add(key); return formJson[key]; @@ -332,12 +270,12 @@ class Form { static Map _parseAdditionalFields( Map formJson, - List parsedJsonFields, + Set parsedFields, PrefixMapping prefixMapping, ) { final additionalFields = {}; for (final entry in formJson.entries) { - if (!parsedJsonFields.contains(entry.key)) { + if (!parsedFields.contains(entry.key)) { final String key = _expandCurieKey(entry.key, prefixMapping); final dynamic value = _expandCurieValue(entry.value, prefixMapping); diff --git a/lib/src/definitions/interaction_affordances/interaction_affordance.dart b/lib/src/definitions/interaction_affordances/interaction_affordance.dart index 1d243293..abf26fc5 100644 --- a/lib/src/definitions/interaction_affordances/interaction_affordance.dart +++ b/lib/src/definitions/interaction_affordances/interaction_affordance.dart @@ -6,6 +6,7 @@ import 'package:curie/curie.dart'; +import '../extensions/json_parser.dart'; import '../form.dart'; import '../thing_description.dart'; @@ -49,24 +50,6 @@ abstract class InteractionAffordance { } } - Map? _parseMultilangString( - Map json, - String jsonKey, - ) { - Map? field; - final dynamic jsonEntries = json[jsonKey]; - if (jsonEntries is Map) { - field = {}; - for (final entry in jsonEntries.entries) { - final dynamic value = entry.value; - if (value is String) { - field[entry.key] = value; - } - } - } - return field; - } - /// Parses the [InteractionAffordance] contained in a [json] object. void parseAffordanceFields( Map json, @@ -74,25 +57,10 @@ abstract class InteractionAffordance { ) { _parseForms(json, prefixMapping); - final dynamic title = json['title']; - if (title is String) { - this.title = title; - } - - titles = _parseMultilangString(json, 'titles'); - - final dynamic description = json['description']; - if (description is String) { - this.description = description; - } - - descriptions = _parseMultilangString(json, 'descriptions'); - - if (json['uriVariables'] != null) { - final dynamic jsonUriVariables = json['uriVariables']; - if (jsonUriVariables is Map) { - uriVariables = jsonUriVariables; - } - } + title = json.parseField('title'); + titles = json.parseMapField('titles'); + description = json.parseField('description'); + descriptions = json.parseMapField('descriptions'); + uriVariables = json.parseMapField('uriVariables'); } } diff --git a/lib/src/definitions/interaction_affordances/property.dart b/lib/src/definitions/interaction_affordances/property.dart index 089930ed..6a8a8149 100644 --- a/lib/src/definitions/interaction_affordances/property.dart +++ b/lib/src/definitions/interaction_affordances/property.dart @@ -7,25 +7,22 @@ import 'package:curie/curie.dart'; import '../data_schema.dart'; +import '../extensions/json_parser.dart'; import '../thing_description.dart'; import 'interaction_affordance.dart'; /// Class representing a [Property] Affordance in a Thing Description. class Property extends InteractionAffordance implements DataSchema { /// Default constructor that creates a [Property] from a [List] of [forms]. - Property(super.forms, super.thingDescription); + Property(super.forms, super.thingDescription, {this.observable = false}); /// Creates a new [Property] from a [json] object. Property.fromJson( Map json, ThingDescription thingDescription, PrefixMapping prefixMapping, - ) : super([], thingDescription) { - final dynamic observable = json['observable']; - if (observable is bool) { - _observable = observable; - } - + ) : observable = json.parseField('observable') ?? false, + super([], thingDescription) { parseAffordanceFields(json, prefixMapping); parseDataSchemaJson(this, json); rawJson = json; @@ -61,12 +58,10 @@ class Property extends InteractionAffordance implements DataSchema { @override bool? writeOnly = false; - bool _observable = false; - /// A hint that indicates whether Servients hosting the Thing and /// Intermediaries should provide a Protocol Binding that supports the /// `observeproperty` and `unobserveproperty` operations for this Property. - bool get observable => _observable; + final bool observable; @override Map? rawJson; diff --git a/lib/src/definitions/link.dart b/lib/src/definitions/link.dart index 6ba21231..0ddcc4a0 100644 --- a/lib/src/definitions/link.dart +++ b/lib/src/definitions/link.dart @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'validation/validation_exception.dart'; +import 'extensions/json_parser.dart'; /// Represents an element of the `links` array in a Thing Description. /// @@ -30,45 +30,21 @@ class Link { /// Creates a new [Link] from a [json] object. Link.fromJson(Map json) { - // TODO(JKRhb): Check if this can be refactored - if (json['href'] is String) { - _parsedJsonFields.add('href'); - final hrefString = json['href'] as String; - href = Uri.parse(hrefString); - } else { - // [href] *must* be initialized. - throw ValidationException("'href' field must exist as a string."); - } - - if (json['type'] is String) { - _parsedJsonFields.add('type'); - type = json['type'] as String; - } - - if (json['rel'] is String) { - _parsedJsonFields.add('rel'); - rel = json['rel'] as String; - } - - if (json['anchor'] is String) { - _parsedJsonFields.add('anchor'); - anchor = Uri.parse(json['anchor'] as String); - } - - if (json['sizes'] is String) { - _parsedJsonFields.add('sizes'); - sizes = json['sizes'] as String; - } - - final dynamic hreflang = json['hreflang']; - _parsedJsonFields.add('hreflang'); - if (hreflang is String) { - this.hreflang = [hreflang]; - } else if (hreflang is List) { - this.hreflang = hreflang.whereType().toList(); - } - - _addAdditionalFields(json); + final Set parsedFields = {}; + + href = Uri.parse(json.parseRequiredField('href', parsedFields)); + type = json.parseField('@type', parsedFields); + rel = json.parseField('rel', parsedFields); + anchor = + Uri.tryParse(json.parseField('anchor', parsedFields) ?? ''); + sizes = json.parseField('sizes', parsedFields); + hreflang = json.parseArrayField('hreflang', parsedFields); + + additionalFields.addAll( + Map.fromEntries( + json.entries.where((element) => !parsedFields.contains(element.key)), + ), + ); } /// Target IRI of a link or submission target of a form. @@ -98,16 +74,6 @@ class Link { /// [BCP47 link]: https://tools.ietf.org/search/bcp47 List? hreflang; - final List _parsedJsonFields = []; - /// Additional fields collected during the parsing of a JSON object. final Map additionalFields = {}; - - void _addAdditionalFields(Map formJson) { - for (final entry in formJson.entries) { - if (!_parsedJsonFields.contains(entry.key)) { - additionalFields[entry.key] = entry.value; - } - } - } } diff --git a/lib/src/definitions/operation_type.dart b/lib/src/definitions/operation_type.dart index 2c92f6af..3475ca3b 100644 --- a/lib/src/definitions/operation_type.dart +++ b/lib/src/definitions/operation_type.dart @@ -43,19 +43,19 @@ enum OperationType { /// Corresponds with the `unsubscribeevent` operation type. unsubscribeevent; - /// Constructor - const OperationType(); + static final Map _registry = + Map.fromEntries(OperationType.values.map((e) => MapEntry(e.name, e))); /// Creates an [OperationType] from a [stringValue]. static OperationType fromString(String stringValue) { - for (final value in OperationType.values) { - if (stringValue == value.name) { - return value; - } + final operationType = OperationType._registry[stringValue]; + + if (operationType == null) { + throw ValidationException( + 'Encountered unknown OperationType $stringValue.', + ); } - throw ValidationException( - 'Encountered unknown OperationType $stringValue.', - ); + return operationType; } } diff --git a/lib/src/definitions/security/ace_security_scheme.dart b/lib/src/definitions/security/ace_security_scheme.dart index f63dce22..ad92e81e 100644 --- a/lib/src/definitions/security/ace_security_scheme.dart +++ b/lib/src/definitions/security/ace_security_scheme.dart @@ -4,12 +4,11 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; +import '../extensions/json_parser.dart'; + import 'security_scheme.dart'; /// Experimental ACE Security Scheme. -// TODO(JKRhb): Check whether an audience field is needed or if this implied by -// the base field/form href. class AceSecurityScheme extends SecurityScheme { /// Constructor. AceSecurityScheme({ @@ -26,36 +25,14 @@ class AceSecurityScheme extends SecurityScheme { /// Creates an [AceSecurityScheme] from a [json] object. AceSecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); - - final dynamic jsonAs = _getJsonValue(json, 'ace:as'); - if (jsonAs is String) { - as = jsonAs; - _parsedJsonFields.add('ace:as'); - } - - final dynamic jsonCnonce = _getJsonValue(json, 'ace:cnonce'); - if (jsonCnonce is bool) { - cnonce = jsonCnonce; - _parsedJsonFields.add('ace:cnonce'); - } - - final dynamic jsonAudience = _getJsonValue(json, 'ace:audience'); - if (jsonAudience is String) { - audience = jsonAudience; - _parsedJsonFields.add('ace:audience'); - } + final Set parsedFields = {}; - final dynamic jsonScopes = _getJsonValue(json, 'ace:scopes'); - if (jsonScopes is String) { - scopes = [jsonScopes]; - _parsedJsonFields.add('ace:scopes'); - } else if (jsonScopes is List) { - scopes = jsonScopes.whereType().toList(); - _parsedJsonFields.add('ace:scopes'); - } + as = json.parseField('ace:as', parsedFields); + cnonce = json.parseField('ace:cnonce', parsedFields); + audience = json.parseField('ace:audience', parsedFields); + scopes = json.parseArrayField('ace:scopes', parsedFields); - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, parsedFields); } @override @@ -77,11 +54,4 @@ class AceSecurityScheme extends SecurityScheme { /// Indicates whether a [cnonce] is required by the Resource Server. bool? cnonce; - - final List _parsedJsonFields = []; - - dynamic _getJsonValue(Map json, String key) { - _parsedJsonFields.add(key); - return json[key]; - } } diff --git a/lib/src/definitions/security/apikey_security_scheme.dart b/lib/src/definitions/security/apikey_security_scheme.dart index 6726bbc6..1667104a 100644 --- a/lib/src/definitions/security/apikey_security_scheme.dart +++ b/lib/src/definitions/security/apikey_security_scheme.dart @@ -4,42 +4,31 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; +import '../extensions/json_parser.dart'; import 'security_scheme.dart'; +const _defaultInValue = 'query'; + /// API key authentication security configuration identified by the Vocabulary /// Term `apikey`. class ApiKeySecurityScheme extends SecurityScheme { /// Constructor. ApiKeySecurityScheme({ - String? description, - String? proxy, + super.description, + super.proxy, this.name, String? in_, - Map? descriptions, - }) : in_ = in_ ?? 'query' { - this.description = description; - this.proxy = proxy; - this.descriptions.addAll(descriptions ?? {}); - } + super.descriptions, + }) : in_ = in_ ?? _defaultInValue; /// Creates a [ApiKeySecurityScheme] from a [json] object. ApiKeySecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); - - final dynamic jsonIn = _getJsonValue(json, 'in'); - if (jsonIn is String) { - in_ = jsonIn; - _parsedJsonFields.add('in'); - } + final Set parsedFields = {}; - final dynamic jsonName = _getJsonValue(json, 'name'); - if (jsonName is String) { - name = jsonName; - _parsedJsonFields.add('name'); - } + name = json.parseField('name', parsedFields); + in_ = json.parseField('in', parsedFields) ?? _defaultInValue; - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, parsedFields); } @override @@ -49,12 +38,5 @@ class ApiKeySecurityScheme extends SecurityScheme { String? name; /// Specifies the location of security authentication information. - late String in_ = 'query'; - - final List _parsedJsonFields = []; - - dynamic _getJsonValue(Map json, String key) { - _parsedJsonFields.add(key); - return json[key]; - } + late String in_; } diff --git a/lib/src/definitions/security/auto_security_scheme.dart b/lib/src/definitions/security/auto_security_scheme.dart index b8987de3..0e273789 100644 --- a/lib/src/definitions/security/auto_security_scheme.dart +++ b/lib/src/definitions/security/auto_security_scheme.dart @@ -4,7 +4,6 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; import 'security_scheme.dart'; /// An automatic security configuration identified by the @@ -12,12 +11,9 @@ import 'security_scheme.dart'; class AutoSecurityScheme extends SecurityScheme { /// Creates an [AutoSecurityScheme] from a [json] object. AutoSecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, {}); } @override String get scheme => 'auto'; - - final List _parsedJsonFields = []; } diff --git a/lib/src/definitions/security/basic_security_scheme.dart b/lib/src/definitions/security/basic_security_scheme.dart index f27879db..b8dd3497 100644 --- a/lib/src/definitions/security/basic_security_scheme.dart +++ b/lib/src/definitions/security/basic_security_scheme.dart @@ -4,57 +4,39 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; +import '../extensions/json_parser.dart'; import 'security_scheme.dart'; +const _defaultInValue = 'header'; + /// Basic Authentication security configuration identified by the Vocabulary /// Term `basic`. class BasicSecurityScheme extends SecurityScheme { /// Constructor. BasicSecurityScheme({ - String? description, - String? proxy, + super.description, + super.proxy, this.name, String? in_, - Map? descriptions, - }) : in_ = in_ ?? 'header' { - this.description = description; - this.proxy = proxy; - this.descriptions.addAll(descriptions ?? {}); - } + super.descriptions, + }) : in_ = in_ ?? _defaultInValue; /// Creates a [BasicSecurityScheme] from a [json] object. BasicSecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); - - final dynamic jsonIn = _getJsonValue(json, 'in'); - if (jsonIn is String) { - in_ = jsonIn; - _parsedJsonFields.add('in'); - } + final Set parsedFields = {}; - final dynamic jsonName = _getJsonValue(json, 'name'); - if (jsonName is String) { - name = jsonName; - _parsedJsonFields.add('name'); - } + name = json.parseField('name', parsedFields); + in_ = json.parseField('in', parsedFields) ?? _defaultInValue; - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, parsedFields); } @override String get scheme => 'basic'; /// Name for query, header, cookie, or uri parameters. - String? name; + late final String? name; /// Specifies the location of security authentication information. late String in_ = 'header'; - - final List _parsedJsonFields = []; - - dynamic _getJsonValue(Map json, String key) { - _parsedJsonFields.add(key); - return json[key]; - } } diff --git a/lib/src/definitions/security/bearer_security_scheme.dart b/lib/src/definitions/security/bearer_security_scheme.dart index d8a7e7e2..1928b485 100644 --- a/lib/src/definitions/security/bearer_security_scheme.dart +++ b/lib/src/definitions/security/bearer_security_scheme.dart @@ -4,89 +4,59 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; +import '../extensions/json_parser.dart'; import 'security_scheme.dart'; +const _defaultInValue = 'header'; +const _defaultAlgValue = 'ES256'; +const _defaultFormatValue = 'jwt'; + /// Bearer Token security configuration identified by the Vocabulary Term /// `bearer`. class BearerSecurityScheme extends SecurityScheme { /// Constructor. BearerSecurityScheme({ - String? description, - String? proxy, this.name, String? alg, String? format, this.authorization, String? in_, - Map? descriptions, - }) : in_ = in_ ?? 'header', - alg = alg ?? 'ES256', - format = format ?? 'jwt' { - this.description = description; - this.proxy = proxy; - this.descriptions.addAll(descriptions ?? {}); - } + super.proxy, + super.description, + super.descriptions, + }) : in_ = in_ ?? _defaultInValue, + alg = alg ?? _defaultAlgValue, + format = format ?? _defaultFormatValue; /// Creates a [BearerSecurityScheme] from a [json] object. BearerSecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); - - final dynamic jsonIn = _getJsonValue(json, 'in'); - if (jsonIn is String) { - in_ = jsonIn; - _parsedJsonFields.add('in'); - } - - final dynamic jsonName = _getJsonValue(json, 'name'); - if (jsonName is String) { - name = jsonName; - _parsedJsonFields.add('name'); - } - - final dynamic jsonFormat = _getJsonValue(json, 'format'); - if (jsonFormat is String) { - format = jsonFormat; - _parsedJsonFields.add('format'); - } + final Set parsedFields = {}; - final dynamic jsonAlg = _getJsonValue(json, 'alg'); - if (jsonAlg is String) { - alg = jsonAlg; - _parsedJsonFields.add('alg'); - } + name = json.parseField('name', parsedFields); + in_ = json.parseField('in', parsedFields) ?? _defaultInValue; + format = + json.parseField('format', parsedFields) ?? _defaultFormatValue; + alg = json.parseField('alg', parsedFields) ?? _defaultAlgValue; + authorization = json.parseField('authorization', parsedFields); - final dynamic jsonAuthorization = _getJsonValue(json, 'authorization'); - if (jsonAuthorization is String) { - authorization = jsonAuthorization; - _parsedJsonFields.add('authorization'); - } - - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, parsedFields); } @override String get scheme => 'bearer'; /// URI of the authorization server. - String? authorization; + late final String? authorization; /// Name for query, header, cookie, or uri parameters. - String? name; + late final String? name; /// Encoding, encryption, or digest algorithm. - String alg = 'ES256'; + late final String alg; /// Specifies format of security authentication information. - String? format = 'jwt'; + late final String format; /// Specifies the location of security authentication information. - String in_ = 'header'; - - final List _parsedJsonFields = []; - - dynamic _getJsonValue(Map json, String key) { - _parsedJsonFields.add(key); - return json[key]; - } + late final String in_; } diff --git a/lib/src/definitions/security/digest_security_scheme.dart b/lib/src/definitions/security/digest_security_scheme.dart index 6ee1c59f..edbcb2ba 100644 --- a/lib/src/definitions/security/digest_security_scheme.dart +++ b/lib/src/definitions/security/digest_security_scheme.dart @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; +import '../extensions/json_parser.dart'; import 'security_scheme.dart'; const _defaultInValue = 'header'; @@ -16,72 +16,35 @@ const _defaultQoPValue = 'auth'; class DigestSecurityScheme extends SecurityScheme { /// Constructor. DigestSecurityScheme({ - String? description, - String? proxy, - this.name, String? in_, String? qop, - Map? descriptions, - }) : _in = in_, - _qop = qop { - this.description = description; - this.proxy = proxy; - this.descriptions.addAll(descriptions ?? {}); - } + super.description, + super.proxy, + this.name, + super.descriptions, + }) : in_ = in_ ?? _defaultInValue, + qop = qop ?? _defaultQoPValue; /// Creates a [DigestSecurityScheme] from a [json] object. - DigestSecurityScheme.fromJson(Map json) - : name = _parseNameJson(json) { - _parsedJsonFields - ..addAll(parseSecurityJson(this, json)) - ..add('name'); + DigestSecurityScheme.fromJson(Map json) { + final Set parsedFields = {}; - final dynamic jsonIn = _getJsonValue(json, 'in', _parsedJsonFields); - if (jsonIn is String) { - _in = jsonIn; - } + name = json.parseField('name', parsedFields); + in_ = json.parseField('in', parsedFields) ?? _defaultInValue; + qop = json.parseField('qop', parsedFields) ?? _defaultInValue; - final dynamic jsonQop = _getJsonValue(json, 'qop', _parsedJsonFields); - if (jsonQop is String) { - _qop = jsonQop; - } - - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, parsedFields); } @override String get scheme => 'digest'; /// Name for query, header, cookie, or uri parameters. - final String? name; - - String? _in; + late final String? name; /// Specifies the location of security authentication information. - String get in_ => _in ?? _defaultInValue; - - String? _qop; + late final String in_; /// Quality of protection. - String get qop => _qop ?? _defaultQoPValue; - - final List _parsedJsonFields = []; - - static dynamic _getJsonValue( - Map json, - String key, [ - List? parsedJsonFields, - ]) { - parsedJsonFields?.add(key); - return json[key]; - } - - static String? _parseNameJson(Map json) { - final dynamic jsonName = _getJsonValue(json, 'name'); - if (jsonName is String) { - return jsonName; - } - - return null; - } + late final String? qop; } diff --git a/lib/src/definitions/security/helper_functions.dart b/lib/src/definitions/security/helper_functions.dart deleted file mode 100644 index 7b0a339e..00000000 --- a/lib/src/definitions/security/helper_functions.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2022 The NAMIB Project Developers. 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 'security_scheme.dart'; - -/// Parses the fields shared by all [SecurityScheme]s. -List parseSecurityJson( - SecurityScheme securityScheme, - Map json, -) { - final List parsedJsonFields = ['scheme']; - - final dynamic proxy = json['proxy']; - if (proxy is String) { - securityScheme.proxy = proxy; - } - - final dynamic description = json['description']; - if (description is String) { - securityScheme.description = description; - } - - final dynamic descriptions = json['descriptions']; - if (descriptions is Map) { - for (final entry in descriptions.entries) { - final dynamic value = entry.value; - if (value is String) { - securityScheme.descriptions[entry.key] = value; - } - } - } - - final dynamic jsonLdType = json['@type']; - if (jsonLdType is String) { - securityScheme.jsonLdType = [jsonLdType]; - } else if (jsonLdType is List) { - securityScheme.jsonLdType = - jsonLdType.whereType().toList(growable: false); - } - - return parsedJsonFields; -} - -/// Parses additional fields which are not part of the WoT specification. -void parseAdditionalFields( - Map additionalFields, - Map json, - List parsedJsonFields, -) { - final additionEntries = json.entries - .where((jsonEntry) => !parsedJsonFields.contains(jsonEntry.key)); - additionalFields.addEntries(additionEntries); -} diff --git a/lib/src/definitions/security/no_security_scheme.dart b/lib/src/definitions/security/no_security_scheme.dart index 9285c10a..0eaff994 100644 --- a/lib/src/definitions/security/no_security_scheme.dart +++ b/lib/src/definitions/security/no_security_scheme.dart @@ -4,7 +4,6 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; import 'security_scheme.dart'; /// A security configuration corresponding to identified by the Vocabulary Term @@ -12,12 +11,9 @@ import 'security_scheme.dart'; class NoSecurityScheme extends SecurityScheme { /// Creates a [NoSecurityScheme] from a [json] object. NoSecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, {}); } @override String get scheme => 'nosec'; - - final List _parsedJsonFields = []; } diff --git a/lib/src/definitions/security/oauth2_security_scheme.dart b/lib/src/definitions/security/oauth2_security_scheme.dart index 18030cf5..9f713e09 100644 --- a/lib/src/definitions/security/oauth2_security_scheme.dart +++ b/lib/src/definitions/security/oauth2_security_scheme.dart @@ -4,8 +4,7 @@ // // SPDX-License-Identifier: BSD-3-Clause -import '../validation/validation_exception.dart'; -import 'helper_functions.dart'; +import '../extensions/json_parser.dart'; import 'security_scheme.dart'; /// OAuth 2.0 authentication security configuration for systems conformant with @@ -28,44 +27,15 @@ class OAuth2SecurityScheme extends SecurityScheme { /// Creates a [OAuth2SecurityScheme] from a [json] object. OAuth2SecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); + final Set parsedFields = {}; - final dynamic jsonAuthorization = _getJsonValue(json, 'authorization'); - if (jsonAuthorization is String) { - authorization = jsonAuthorization; - _parsedJsonFields.add('authorization'); - } + authorization = json.parseField('authorization', parsedFields); + token = json.parseField('token', parsedFields); + refresh = json.parseField('refresh', parsedFields); + scopes = json.parseArrayField('scopes', parsedFields); + flow = json.parseRequiredField('flow', parsedFields); - final dynamic jsonToken = _getJsonValue(json, 'token'); - if (jsonToken is String) { - token = jsonToken; - _parsedJsonFields.add('token'); - } - - final dynamic jsonRefresh = _getJsonValue(json, 'refresh'); - if (jsonRefresh is String) { - refresh = jsonRefresh; - _parsedJsonFields.add('refresh'); - } - - final dynamic jsonScopes = _getJsonValue(json, 'scopes'); - if (jsonScopes is String) { - scopes = [jsonScopes]; - _parsedJsonFields.add('scopes'); - } else if (jsonScopes is List) { - scopes = jsonScopes.whereType().toList(growable: false); - _parsedJsonFields.add('scopes'); - } - - final dynamic jsonFlow = _getJsonValue(json, 'flow'); - if (jsonFlow is String) { - flow = jsonFlow; - _parsedJsonFields.add('flow'); - } else { - throw ValidationException("flow must be of type 'string'!"); - } - - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, parsedFields); } @override String get scheme => 'oauth2'; @@ -74,13 +44,13 @@ class OAuth2SecurityScheme extends SecurityScheme { /// /// In the case of the `device` flow, the URI provided for the [authorization] /// value refers to the device authorization endpoint. - String? authorization; + late String? authorization; /// URI of the token server. - String? token; + late String? token; /// URI of the authorization server. - String? refresh; + late String? refresh; /// Set of authorization scope identifiers provided as an array. /// @@ -88,15 +58,8 @@ class OAuth2SecurityScheme extends SecurityScheme { /// associated with forms in order to identify what resources a client may /// access and how. The values associated with a form should be chosen from /// those defined in an [OAuth2SecurityScheme] active on that form. - List? scopes; + late List? scopes; /// Authorization flow. late String flow; - - final List _parsedJsonFields = []; - - dynamic _getJsonValue(Map json, String key) { - _parsedJsonFields.add(key); - return json[key]; - } } diff --git a/lib/src/definitions/security/psk_security_scheme.dart b/lib/src/definitions/security/psk_security_scheme.dart index f298a038..1e715b3a 100644 --- a/lib/src/definitions/security/psk_security_scheme.dart +++ b/lib/src/definitions/security/psk_security_scheme.dart @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: BSD-3-Clause -import 'helper_functions.dart'; +import '../extensions/json_parser.dart'; import 'security_scheme.dart'; /// Pre-shared key authentication security configuration identified by the @@ -24,15 +24,11 @@ class PskSecurityScheme extends SecurityScheme { /// Creates a [PskSecurityScheme] from a [json] object. PskSecurityScheme.fromJson(Map json) { - _parsedJsonFields.addAll(parseSecurityJson(this, json)); + final Set parsedFields = {}; - final dynamic jsonIdentity = _getJsonValue(json, 'identity'); - if (jsonIdentity is String) { - identity = jsonIdentity; - _parsedJsonFields.add('identity'); - } + identity = json.parseField('identity'); - parseAdditionalFields(additionalFields, json, _parsedJsonFields); + parseSecurityJson(json, parsedFields); } @override @@ -40,11 +36,4 @@ class PskSecurityScheme extends SecurityScheme { /// Name for query, header, cookie, or uri parameters. String? identity; - - final List _parsedJsonFields = []; - - dynamic _getJsonValue(Map json, String key) { - _parsedJsonFields.add(key); - return json[key]; - } } diff --git a/lib/src/definitions/security/security_scheme.dart b/lib/src/definitions/security/security_scheme.dart index f6983365..d01774ee 100644 --- a/lib/src/definitions/security/security_scheme.dart +++ b/lib/src/definitions/security/security_scheme.dart @@ -4,9 +4,29 @@ // // SPDX-License-Identifier: BSD-3-Clause +import '../extensions/json_parser.dart'; +import 'ace_security_scheme.dart'; +import 'apikey_security_scheme.dart'; +import 'auto_security_scheme.dart'; +import 'basic_security_scheme.dart'; +import 'bearer_security_scheme.dart'; +import 'digest_security_scheme.dart'; +import 'no_security_scheme.dart'; +import 'oauth2_security_scheme.dart'; +import 'psk_security_scheme.dart'; + /// Class that contains metadata describing the configuration of a security /// mechanism. abstract class SecurityScheme { + /// Constructor. + SecurityScheme({ + this.description, + this.proxy, + Map? descriptions, + }) { + this.descriptions.addAll(descriptions ?? {}); + } + /// The actual security [scheme] identifier. /// /// Can be one of `nosec`, `combo`, `basic`, `digest`, `bearer`, `psk`, @@ -17,7 +37,7 @@ abstract class SecurityScheme { String? description; /// A [Map] of multi-language [descriptions]. - final Map descriptions = {}; + Map descriptions = {}; /// String? proxy; @@ -27,4 +47,48 @@ abstract class SecurityScheme { /// Additional fields collected during the parsing of a JSON object. final Map additionalFields = {}; + + /// Parses the fields shared by all [SecurityScheme]s. + void parseSecurityJson( + Map json, + Set parsedFields, + ) { + parsedFields.add('scheme'); + + proxy = json.parseField('proxy', parsedFields); + description = json.parseField('description', parsedFields); + descriptions + .addAll(json.parseMapField('descriptions', parsedFields) ?? {}); + jsonLdType = json.parseArrayField('@type'); + + additionalFields.addEntries( + json.entries.where((jsonEntry) => !parsedFields.contains(jsonEntry.key)), + ); + } + + /// Creates a [SecurityScheme] from a [json] object. + static SecurityScheme? fromJson(Map json) { + switch (json['scheme']) { + case 'auto': + return AutoSecurityScheme.fromJson(json); + case 'basic': + return BasicSecurityScheme.fromJson(json); + case 'bearer': + return BearerSecurityScheme.fromJson(json); + case 'nosec': + return NoSecurityScheme.fromJson(json); + case 'psk': + return PskSecurityScheme.fromJson(json); + case 'digest': + return DigestSecurityScheme.fromJson(json); + case 'apikey': + return ApiKeySecurityScheme.fromJson(json); + case 'oauth2': + return OAuth2SecurityScheme.fromJson(json); + case 'ace:ACESecurityScheme': + return AceSecurityScheme.fromJson(json); + } + + return null; + } } diff --git a/lib/src/definitions/thing_description.dart b/lib/src/definitions/thing_description.dart index 1301db33..aeed2825 100644 --- a/lib/src/definitions/thing_description.dart +++ b/lib/src/definitions/thing_description.dart @@ -9,29 +9,14 @@ import 'dart:convert'; import 'package:curie/curie.dart'; import 'context_entry.dart'; +import 'extensions/json_parser.dart'; import 'interaction_affordances/action.dart'; import 'interaction_affordances/event.dart'; import 'interaction_affordances/property.dart'; import 'link.dart'; -import 'security/ace_security_scheme.dart'; -import 'security/apikey_security_scheme.dart'; -import 'security/auto_security_scheme.dart'; -import 'security/basic_security_scheme.dart'; -import 'security/bearer_security_scheme.dart'; -import 'security/digest_security_scheme.dart'; -import 'security/no_security_scheme.dart'; -import 'security/oauth2_security_scheme.dart'; -import 'security/psk_security_scheme.dart'; import 'security/security_scheme.dart'; import 'thing_model.dart'; import 'validation/thing_description_schema.dart'; -import 'validation/validation_exception.dart'; - -const _validContextValues = [ - 'https://www.w3.org/2019/wot/td/v1', - 'https://www.w3.org/2022/wot/td/v1.1', - 'http://www.w3.org/ns/td' -]; /// Represents a WoT Thing Description class ThingDescription { @@ -140,36 +125,27 @@ class ThingDescription { } void _parseJson(Map json) { - _parseTitle(json['title']); - _parseContext(json['@context']); - final dynamic id = json['id']; - if (id is String) { - this.id = id; - } - final dynamic base = json['base']; - if (base is String) { - this.base = Uri.parse(base); - } - final dynamic description = json['description']; - if (description is String) { - this.description = description; - } - _parseMultilangString(titles, json, 'titles'); - _parseMultilangString(descriptions, json, 'descriptions'); - final dynamic security = json['security']; - if (security is List) { - this.security.addAll(security.whereType()); - } else if (security is String) { - this.security.add(security); - } + // TODO: Move to constructor? + final Set parsedFields = {}; + + context.addAll(ContextEntry.parseContext(json['@context'], prefixMapping)); + title = json.parseRequiredField('title', parsedFields); + titles.addAll(json.parseMapField('titles', parsedFields) ?? {}); + description = json.parseField('description', parsedFields); + descriptions + .addAll(json.parseMapField('descriptions', parsedFields) ?? {}); + id = json.parseField('id', parsedFields); + base = Uri.tryParse(json.parseField('base', parsedFields) ?? ''); + security + .addAll(json.parseArrayField('security', parsedFields) ?? []); + final dynamic securityDefinitions = json['securityDefinitions']; if (securityDefinitions is Map) { _parseSecurityDefinitions(securityDefinitions); } - final dynamic jsonUriVariables = json['uriVariables']; - if (jsonUriVariables is Map) { - uriVariables = jsonUriVariables; - } + + uriVariables = json.parseMapField('uriVariables'); + final dynamic properties = json['properties']; if (properties is Map) { _parseProperties(properties); @@ -188,72 +164,6 @@ class ThingDescription { } } - // TODO(JKRhb): Refactor - void _parseMultilangString( - Map field, - Map json, - String jsonKey, - ) { - final dynamic jsonEntries = json[jsonKey]; - if (jsonEntries is Map) { - for (final entry in jsonEntries.entries) { - final dynamic value = entry.value; - if (value is String) { - field[entry.key] = value; - } - } - } - } - - void _parseTitle(dynamic titleJson) { - if (titleJson is String) { - title = titleJson; - } else { - throw ValidationException( - 'Thing Description type is not a ' - 'String but ${title.runtimeType}', - ); - } - } - - void _parseContext(dynamic contextJson) { - if (contextJson is String || contextJson is Map) { - _parseContextListEntry(contextJson); - } else if (contextJson is List) { - var firstEntry = true; - for (final contextEntry in contextJson) { - _parseContextListEntry(contextEntry, firstEntry: firstEntry); - if (contextEntry is String && - _validContextValues.contains(contextEntry)) { - firstEntry = false; - } - } - } - } - - void _parseContextListEntry( - dynamic contextJsonListEntry, { - bool firstEntry = false, - }) { - if (contextJsonListEntry is String) { - context.add(ContextEntry(contextJsonListEntry, null)); - if (firstEntry && _validContextValues.contains(contextJsonListEntry)) { - prefixMapping.defaultPrefixValue = contextJsonListEntry; - } - } else if (contextJsonListEntry is Map) { - for (final mapEntry in contextJsonListEntry.entries) { - final dynamic value = mapEntry.value; - final key = mapEntry.key; - if (value is String) { - context.add(ContextEntry(value, key)); - if (!key.startsWith('@') && Uri.tryParse(value) != null) { - prefixMapping.addPrefix(key, value); - } - } - } - } - } - void _parseLinks(List json) { for (final link in json) { if (link is Map) { @@ -294,57 +204,10 @@ class ThingDescription { for (final securityDefinition in json.entries) { final dynamic value = securityDefinition.value; if (value is Map) { - SecurityScheme securityScheme; - switch (value['scheme']) { - case 'auto': - { - securityScheme = AutoSecurityScheme.fromJson(value); - break; - } - case 'basic': - { - securityScheme = BasicSecurityScheme.fromJson(value); - break; - } - case 'bearer': - { - securityScheme = BearerSecurityScheme.fromJson(value); - break; - } - case 'nosec': - { - securityScheme = NoSecurityScheme.fromJson(value); - break; - } - case 'psk': - { - securityScheme = PskSecurityScheme.fromJson(value); - break; - } - case 'digest': - { - securityScheme = DigestSecurityScheme.fromJson(value); - break; - } - case 'apikey': - { - securityScheme = ApiKeySecurityScheme.fromJson(value); - break; - } - case 'oauth2': - { - securityScheme = OAuth2SecurityScheme.fromJson(value); - break; - } - case 'ace:ACESecurityScheme': - { - securityScheme = AceSecurityScheme.fromJson(value); - break; - } - default: - continue; + final securityScheme = SecurityScheme.fromJson(value); + if (securityScheme != null) { + securityDefinitions[securityDefinition.key] = securityScheme; } - securityDefinitions[securityDefinition.key] = securityScheme; } } } diff --git a/test/core/dart_wot_test.dart b/test/core/dart_wot_test.dart index f08f57fa..9e4fcf99 100644 --- a/test/core/dart_wot_test.dart +++ b/test/core/dart_wot_test.dart @@ -20,6 +20,7 @@ void main() { final servient = Servient(); final wot = await servient.start(); final Map exposedThingInit = { + '@context': 'https://www.w3.org/2022/wot/td/v1.1', 'title': 'Test Thing' }; final dynamic exposedThing = await wot.produce(exposedThingInit); @@ -46,7 +47,7 @@ void main() { "href": "https://example.org", "rel": "test", "anchor": "https://example.org", - "type": "test", + "@type": "test", "sizes": "42", "test": "test", "hreflang": "de" diff --git a/test/core/definitions_test.dart b/test/core/definitions_test.dart index 96517a80..7a706bd4 100644 --- a/test/core/definitions_test.dart +++ b/test/core/definitions_test.dart @@ -10,11 +10,19 @@ import 'package:dart_wot/dart_wot.dart'; import 'package:dart_wot/src/definitions/additional_expected_response.dart'; import 'package:dart_wot/src/definitions/context_entry.dart'; import 'package:dart_wot/src/definitions/expected_response.dart'; +import 'package:dart_wot/src/definitions/interaction_affordances/interaction_affordance.dart'; import 'package:dart_wot/src/definitions/interaction_affordances/property.dart'; import 'package:dart_wot/src/definitions/operation_type.dart'; +import 'package:dart_wot/src/definitions/security/auto_security_scheme.dart'; +import 'package:dart_wot/src/definitions/security/no_security_scheme.dart'; import 'package:dart_wot/src/definitions/validation/thing_description_schema.dart'; +import 'package:dart_wot/src/definitions/validation/validation_exception.dart'; import 'package:test/test.dart'; +class _InvalidInteractionAffordance extends InteractionAffordance { + _InvalidInteractionAffordance(super.forms, super.thingDescription); +} + void main() { group('Definitions', () { setUp(() { @@ -182,6 +190,14 @@ void main() { expect(additionalResponse2.contentType, 'text/plain'); expect(additionalResponse2.schema, null); + + expect( + () => Form( + Uri.parse('http://example.org'), + _InvalidInteractionAffordance([], thingDescription), + ), + throwsStateError, + ); }); test('should correctly parse actions', () { @@ -228,13 +244,24 @@ void main() { 'title': 'MyLampThing', 'security': 'nosec_sc', 'securityDefinitions': { - 'nosec_sc': {'scheme': 'nosec'} + 'nosec_sc': {'scheme': 'nosec'}, + 'auto_sc': {'scheme': 'auto'}, }, 'properties': { 'property': { + 'title': 'Test', + 'titles': {'de': 'German Test', 'en': 'English Test'}, + 'description': 'This is a Test', + 'descriptions': { + 'es': 'Esto es una prueba', + 'en': 'This is a Test' + }, 'writeOnly': true, 'readOnly': true, 'observable': true, + 'enum': ['On', 'Off', 3], + 'constant': 'On', + 'type': 'string', 'forms': [ {'href': 'https://example.org'} ] @@ -243,22 +270,57 @@ void main() { 'forms': [ {'href': 'https://example.org'} ] + }, + 'objectSchemeProperty': { + 'properties': { + 'test': {'type': 'string'} + }, + 'forms': [ + { + 'href': 'https://example.org', + 'security': 'auto_sc', + } + ], } } }; final thingDescription = ThingDescription.fromJson(validThingDescription); + expect(thingDescription.security[0], 'nosec_sc'); + final noSecurityScheme = thingDescription.securityDefinitions['nosec_sc']; + expect(noSecurityScheme, isA()); + expect(noSecurityScheme?.scheme, 'nosec'); + final property = thingDescription.properties['property']; + expect(property?.title, 'Test'); + expect(property?.description, 'This is a Test'); + expect(property?.descriptions?['es'], 'Esto es una prueba'); + expect(property?.descriptions?['en'], 'This is a Test'); expect(property?.writeOnly, true); expect(property?.readOnly, true); expect(property?.observable, true); + expect(property?.enumeration, ['On', 'Off', 3]); + expect(property?.constant, 'On'); final propertyWithDefaults = thingDescription.properties['propertyWithDefaults']; expect(propertyWithDefaults?.writeOnly, false); expect(propertyWithDefaults?.readOnly, false); expect(propertyWithDefaults?.observable, false); + + final objectSchemeProperty = + thingDescription.properties['objectSchemeProperty']; + + expect(objectSchemeProperty?.forms[0].security, ['auto_sc']); + final autoSecurityScheme = + objectSchemeProperty?.forms[0].securityDefinitions[0]; + expect(autoSecurityScheme, isA()); + expect(autoSecurityScheme?.scheme, 'auto'); + + final testSchema = objectSchemeProperty?.properties?['test']; + expect(testSchema, isA()); + expect(testSchema?.type, 'string'); }); }); @@ -327,4 +389,70 @@ void main() { 'ace:ACESecurityScheme', ); }); + + test('Should only parse allowed Operation Types', () { + expect( + OperationType.fromString('invokeaction'), + OperationType.invokeaction, + ); + + expect( + () => OperationType.fromString('test'), + throwsA(isA()), + ); + }); + + test('Should correctly parse ExpectedResponse', () { + final firstResponse = ExpectedResponse( + 'application/json', + additionalFields: {'test': 'test'}, + ); + + expect(firstResponse.additionalFields['test'], 'test'); + + final expectedResponseJson = { + 'response': { + 'contentType': 'application/json', + 'test': 'test', + }, + }; + + final secondResponse = ExpectedResponse.fromJson(expectedResponseJson); + + expect(secondResponse, isA()); + expect(secondResponse?.additionalFields['test'], 'test'); + }); + + test('Should reject invalid @context entries', () { + final invalidThingDescription1 = { + '@context': 5, + 'title': 'Test', + 'security': 'nosec_sc', + 'securityDefinitions': { + 'nosec_sc': {'scheme': 'nosec'} + }, + }; + + expect( + () => ThingDescription.fromJson( + invalidThingDescription1, + validate: false, + ), + throwsA(isA()), + ); + + final invalidThingDescription2 = { + '@context': ['https://www.w3.org/2022/wot/td/v1.1', 5], + 'title': 'Test', + 'security': 'nosec_sc', + 'securityDefinitions': { + 'nosec_sc': {'scheme': 'nosec'} + }, + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription2), + throwsA(isA()), + ); + }); }