diff --git a/lib/src/definitions/context_entry.dart b/lib/src/definitions/context_entry.dart deleted file mode 100644 index 6be49079..00000000 --- a/lib/src/definitions/context_entry.dart +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2021 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 '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 -class ContextEntry { - /// Creates a new [ContextEntry]. - const ContextEntry(this.value, this.key); - - /// Parses a [List] of `@context` entries from a given [json] value. - /// - /// `@context` extensions are added to the provided [prefixMapping]. - /// If a given entry is the [firstEntry], it will be set in the - /// [prefixMapping] accordingly. - static List fromJson( - dynamic json, - PrefixMapping prefixMapping, { - required bool firstEntry, - }) { - // TODO: Refactor - if (json is String) { - if (firstEntry && _validTdContextValues.contains(json)) { - prefixMapping.defaultPrefixValue = json; - } - return [ContextEntry(json, null)]; - } - - if (json is Map) { - final contextEntries = []; - 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); - } - contextEntries.add(ContextEntry(value, key)); - } - } - - return contextEntries; - } - - 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.addAll( - 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; - - /// The [key] of this [ContextEntry]. Might be `null`. - final String? key; - - @override - bool operator ==(Object? other) { - return hashCode == other.hashCode; - } - - @override - int get hashCode => Object.hash(value, key); -} diff --git a/lib/src/definitions/extensions/json_parser.dart b/lib/src/definitions/extensions/json_parser.dart index 9eaa3a16..2cedaad8 100644 --- a/lib/src/definitions/extensions/json_parser.dart +++ b/lib/src/definitions/extensions/json_parser.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:curie/curie.dart'; import '../additional_expected_response.dart'; @@ -24,6 +25,12 @@ import '../thing_description.dart'; import '../validation/validation_exception.dart'; import '../version_info.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' +]; + /// Extension for parsing fields of JSON objects. extension ParseField on Map { dynamic _processFieldName(String name, Set? parsedFields) { @@ -583,4 +590,80 @@ extension ParseField on Map { return value; } + + /// Parses the JSON-LD @context of a TD and returns a [List] of + /// [ContextEntry]s. + List parseContext( + PrefixMapping prefixMapping, + Set? parsedFields, { + bool firstEntry = true, + }) { + final fieldValue = parseField('@context', parsedFields); + + return _parseContext(fieldValue, prefixMapping); + } +} + +/// Parses a [List] of `@context` entries from a given [json] value. +/// +/// `@context` extensions are added to the provided [prefixMapping]. +/// If a given entry is the [firstEntry], it will be set in the +/// [prefixMapping] accordingly. +List _parseContext( + dynamic json, + PrefixMapping prefixMapping, { + bool firstEntry = true, +}) { + switch (json) { + case final String jsonString: + { + if (firstEntry && _validTdContextValues.contains(jsonString)) { + prefixMapping.defaultPrefixValue = jsonString; + } + return [(key: null, value: jsonString)]; + } + case final List contextList: + { + final List result = []; + contextList + .mapIndexed( + (index, contextEntry) => _parseContext( + contextEntry, + prefixMapping, + firstEntry: index == 0, + ), + ) + .forEach(result.addAll); + return result; + } + case final Map contextList: + { + return contextList.entries.map((entry) { + final key = entry.key; + final value = entry.value; + + if (value is! String) { + throw ContextValidationException(value.runtimeType); + } + + if (!key.startsWith('@') && Uri.tryParse(value) != null) { + prefixMapping.addPrefix(key, value); + } + return (key: key, value: value); + }).toList(); + } + } + + throw ContextValidationException(json.runtimeType); +} + +/// Custom [ValidationException] that is thrown for an invalid [ContextEntry]. +class ContextValidationException extends ValidationException { + /// Creates a new [ContextValidationException] indicating the invalid + /// [runtimeType]. + ContextValidationException(Type runtimeType) + : super( + 'Excepted either a String or a Map ' + 'as @context entry, got $runtimeType instead.', + ); } diff --git a/lib/src/definitions/thing_description.dart b/lib/src/definitions/thing_description.dart index d5fe1836..15663763 100644 --- a/lib/src/definitions/thing_description.dart +++ b/lib/src/definitions/thing_description.dart @@ -9,7 +9,6 @@ import 'dart:convert'; import 'package:curie/curie.dart'; import 'additional_expected_response.dart'; -import 'context_entry.dart'; import 'data_schema.dart'; import 'extensions/json_parser.dart'; import 'form.dart'; @@ -22,6 +21,9 @@ import 'thing_model.dart'; import 'validation/thing_description_schema.dart'; import 'version_info.dart'; +/// Type definition for a JSON-LD @context entry. +typedef ContextEntry = ({String? key, String value}); + /// Represents a WoT Thing Description class ThingDescription { /// Creates a [ThingDescription] from a [rawThingDescription] JSON [String]. @@ -172,7 +174,7 @@ class ThingDescription { void _parseJson(Map json) { final Set parsedFields = {}; - context.addAll(ContextEntry.parseContext(json['@context'], prefixMapping)); + context.addAll(json.parseContext(prefixMapping, parsedFields)); title = json.parseRequiredField('title', parsedFields); titles.addAll(json.parseMapField('titles', parsedFields) ?? {}); description = json.parseField('description', parsedFields); diff --git a/test/core/definitions_test.dart b/test/core/definitions_test.dart index 4bd6d9a6..06c9de94 100644 --- a/test/core/definitions_test.dart +++ b/test/core/definitions_test.dart @@ -9,7 +9,6 @@ import 'dart:convert'; import 'package:curie/curie.dart'; 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/data_schema.dart'; import 'package:dart_wot/src/definitions/expected_response.dart'; import 'package:dart_wot/src/definitions/extensions/json_parser.dart'; @@ -66,7 +65,7 @@ void main() { expect(thingDescription.title, 'MyLampThing'); expect( thingDescription.context, - [const ContextEntry('https://www.w3.org/2022/wot/td/v1.1', null)], + [const (key: null, value: 'https://www.w3.org/2022/wot/td/v1.1')], ); expect(thingDescription.security, ['nosec_sc']); expect(thingDescription.securityDefinitions['nosec_sc']?.scheme, 'nosec'); @@ -565,4 +564,26 @@ void main() { throwsA(isA()), ); }); + + test('Should reject invalid @context entries', () { + // TODO(JKRhb): Double-check if this the correct behavior. + final invalidThingDescription1 = { + '@context': [ + 'https://www.w3.org/2022/wot/td/v1.1', + {'invalid': 1} + ], + 'title': 'NAMIB WoT Thing', + 'security': ['nosec_sc'], + 'securityDefinitions': { + 'nosec_sc': { + 'scheme': 'nosec', + } + } + }; + + expect( + () => ThingDescription.fromJson(invalidThingDescription1), + throwsA(isA()), + ); + }); }