Skip to content

Commit

Permalink
refactor!: rework @context parsing using record type
Browse files Browse the repository at this point in the history
  • Loading branch information
JKRhb committed May 13, 2023
1 parent dd5c8ff commit b6f95de
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 116 deletions.
112 changes: 0 additions & 112 deletions lib/src/definitions/context_entry.dart

This file was deleted.

83 changes: 83 additions & 0 deletions lib/src/definitions/extensions/json_parser.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:curie/curie.dart';

import '../additional_expected_response.dart';
Expand All @@ -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<String, dynamic> {
dynamic _processFieldName(String name, Set<String>? parsedFields) {
Expand Down Expand Up @@ -583,4 +590,80 @@ extension ParseField on Map<String, dynamic> {

return value;
}

/// Parses the JSON-LD @context of a TD and returns a [List] of
/// [ContextEntry]s.
List<ContextEntry> parseContext(
PrefixMapping prefixMapping,
Set<String>? 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<ContextEntry> _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<dynamic> contextList:
{
final List<ContextEntry> result = [];
contextList
.mapIndexed(
(index, contextEntry) => _parseContext(
contextEntry,
prefixMapping,
firstEntry: index == 0,
),
)
.forEach(result.addAll);
return result;
}
case final Map<String, dynamic> 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<String, String> '
'as @context entry, got $runtimeType instead.',
);
}
6 changes: 4 additions & 2 deletions lib/src/definitions/thing_description.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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].
Expand Down Expand Up @@ -172,7 +174,7 @@ class ThingDescription {
void _parseJson(Map<String, dynamic> json) {
final Set<String> parsedFields = {};

context.addAll(ContextEntry.parseContext(json['@context'], prefixMapping));
context.addAll(json.parseContext(prefixMapping, parsedFields));
title = json.parseRequiredField<String>('title', parsedFields);
titles.addAll(json.parseMapField<String>('titles', parsedFields) ?? {});
description = json.parseField<String>('description', parsedFields);
Expand Down
25 changes: 23 additions & 2 deletions test/core/definitions_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -565,4 +564,26 @@ void main() {
throwsA(isA<ValidationException>()),
);
});

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<ContextValidationException>()),
);
});
}

0 comments on commit b6f95de

Please sign in to comment.