Skip to content

Commit b6f95de

Browse files
committed
refactor!: rework @context parsing using record type
1 parent dd5c8ff commit b6f95de

File tree

4 files changed

+110
-116
lines changed

4 files changed

+110
-116
lines changed

lib/src/definitions/context_entry.dart

Lines changed: 0 additions & 112 deletions
This file was deleted.

lib/src/definitions/extensions/json_parser.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:collection/collection.dart';
12
import 'package:curie/curie.dart';
23

34
import '../additional_expected_response.dart';
@@ -24,6 +25,12 @@ import '../thing_description.dart';
2425
import '../validation/validation_exception.dart';
2526
import '../version_info.dart';
2627

28+
const _validTdContextValues = [
29+
'https://www.w3.org/2019/wot/td/v1',
30+
'https://www.w3.org/2022/wot/td/v1.1',
31+
'http://www.w3.org/ns/td'
32+
];
33+
2734
/// Extension for parsing fields of JSON objects.
2835
extension ParseField on Map<String, dynamic> {
2936
dynamic _processFieldName(String name, Set<String>? parsedFields) {
@@ -583,4 +590,80 @@ extension ParseField on Map<String, dynamic> {
583590

584591
return value;
585592
}
593+
594+
/// Parses the JSON-LD @context of a TD and returns a [List] of
595+
/// [ContextEntry]s.
596+
List<ContextEntry> parseContext(
597+
PrefixMapping prefixMapping,
598+
Set<String>? parsedFields, {
599+
bool firstEntry = true,
600+
}) {
601+
final fieldValue = parseField('@context', parsedFields);
602+
603+
return _parseContext(fieldValue, prefixMapping);
604+
}
605+
}
606+
607+
/// Parses a [List] of `@context` entries from a given [json] value.
608+
///
609+
/// `@context` extensions are added to the provided [prefixMapping].
610+
/// If a given entry is the [firstEntry], it will be set in the
611+
/// [prefixMapping] accordingly.
612+
List<ContextEntry> _parseContext(
613+
dynamic json,
614+
PrefixMapping prefixMapping, {
615+
bool firstEntry = true,
616+
}) {
617+
switch (json) {
618+
case final String jsonString:
619+
{
620+
if (firstEntry && _validTdContextValues.contains(jsonString)) {
621+
prefixMapping.defaultPrefixValue = jsonString;
622+
}
623+
return [(key: null, value: jsonString)];
624+
}
625+
case final List<dynamic> contextList:
626+
{
627+
final List<ContextEntry> result = [];
628+
contextList
629+
.mapIndexed(
630+
(index, contextEntry) => _parseContext(
631+
contextEntry,
632+
prefixMapping,
633+
firstEntry: index == 0,
634+
),
635+
)
636+
.forEach(result.addAll);
637+
return result;
638+
}
639+
case final Map<String, dynamic> contextList:
640+
{
641+
return contextList.entries.map((entry) {
642+
final key = entry.key;
643+
final value = entry.value;
644+
645+
if (value is! String) {
646+
throw ContextValidationException(value.runtimeType);
647+
}
648+
649+
if (!key.startsWith('@') && Uri.tryParse(value) != null) {
650+
prefixMapping.addPrefix(key, value);
651+
}
652+
return (key: key, value: value);
653+
}).toList();
654+
}
655+
}
656+
657+
throw ContextValidationException(json.runtimeType);
658+
}
659+
660+
/// Custom [ValidationException] that is thrown for an invalid [ContextEntry].
661+
class ContextValidationException extends ValidationException {
662+
/// Creates a new [ContextValidationException] indicating the invalid
663+
/// [runtimeType].
664+
ContextValidationException(Type runtimeType)
665+
: super(
666+
'Excepted either a String or a Map<String, String> '
667+
'as @context entry, got $runtimeType instead.',
668+
);
586669
}

lib/src/definitions/thing_description.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import 'dart:convert';
99
import 'package:curie/curie.dart';
1010

1111
import 'additional_expected_response.dart';
12-
import 'context_entry.dart';
1312
import 'data_schema.dart';
1413
import 'extensions/json_parser.dart';
1514
import 'form.dart';
@@ -22,6 +21,9 @@ import 'thing_model.dart';
2221
import 'validation/thing_description_schema.dart';
2322
import 'version_info.dart';
2423

24+
/// Type definition for a JSON-LD @context entry.
25+
typedef ContextEntry = ({String? key, String value});
26+
2527
/// Represents a WoT Thing Description
2628
class ThingDescription {
2729
/// Creates a [ThingDescription] from a [rawThingDescription] JSON [String].
@@ -172,7 +174,7 @@ class ThingDescription {
172174
void _parseJson(Map<String, dynamic> json) {
173175
final Set<String> parsedFields = {};
174176

175-
context.addAll(ContextEntry.parseContext(json['@context'], prefixMapping));
177+
context.addAll(json.parseContext(prefixMapping, parsedFields));
176178
title = json.parseRequiredField<String>('title', parsedFields);
177179
titles.addAll(json.parseMapField<String>('titles', parsedFields) ?? {});
178180
description = json.parseField<String>('description', parsedFields);

test/core/definitions_test.dart

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import 'dart:convert';
99
import 'package:curie/curie.dart';
1010
import 'package:dart_wot/dart_wot.dart';
1111
import 'package:dart_wot/src/definitions/additional_expected_response.dart';
12-
import 'package:dart_wot/src/definitions/context_entry.dart';
1312
import 'package:dart_wot/src/definitions/data_schema.dart';
1413
import 'package:dart_wot/src/definitions/expected_response.dart';
1514
import 'package:dart_wot/src/definitions/extensions/json_parser.dart';
@@ -66,7 +65,7 @@ void main() {
6665
expect(thingDescription.title, 'MyLampThing');
6766
expect(
6867
thingDescription.context,
69-
[const ContextEntry('https://www.w3.org/2022/wot/td/v1.1', null)],
68+
[const (key: null, value: 'https://www.w3.org/2022/wot/td/v1.1')],
7069
);
7170
expect(thingDescription.security, ['nosec_sc']);
7271
expect(thingDescription.securityDefinitions['nosec_sc']?.scheme, 'nosec');
@@ -565,4 +564,26 @@ void main() {
565564
throwsA(isA<ValidationException>()),
566565
);
567566
});
567+
568+
test('Should reject invalid @context entries', () {
569+
// TODO(JKRhb): Double-check if this the correct behavior.
570+
final invalidThingDescription1 = {
571+
'@context': [
572+
'https://www.w3.org/2022/wot/td/v1.1',
573+
{'invalid': 1}
574+
],
575+
'title': 'NAMIB WoT Thing',
576+
'security': ['nosec_sc'],
577+
'securityDefinitions': {
578+
'nosec_sc': {
579+
'scheme': 'nosec',
580+
}
581+
}
582+
};
583+
584+
expect(
585+
() => ThingDescription.fromJson(invalidThingDescription1),
586+
throwsA(isA<ContextValidationException>()),
587+
);
588+
});
568589
}

0 commit comments

Comments
 (0)