Skip to content

Commit

Permalink
refactor: rework data schema implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
JKRhb committed Sep 22, 2022
1 parent 33a255e commit 506d20b
Show file tree
Hide file tree
Showing 24 changed files with 596 additions and 757 deletions.
64 changes: 30 additions & 34 deletions lib/src/definitions/additional_expected_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,62 @@
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
class AdditionalExpectedResponse {
/// Constructs a new [AdditionalExpectedResponse] object from a [contentType].
AdditionalExpectedResponse(
this.contentType, {
String? schema,
this.schema,
bool? success,
}) : _success = success,
_schema = schema;
Map<String, dynamic>? additionalFields,
}) : additionalFields = additionalFields ?? {},
success = success ?? false;

/// Creates an [AdditionalExpectedResponse] from a [json] object.
AdditionalExpectedResponse.fromJson(
factory AdditionalExpectedResponse.fromJson(
Map<String, dynamic> 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<T>(Map<String, dynamic> json, String key) {
final dynamic value = json[key];

if (value is T) {
return value;
}

return null;
) {
final Set<String> parsedFields = {};

final contentType =
json.parseField<String>('contentType', parsedFields) ?? formContentType;
final success = json.parseField<bool>('success', parsedFields);
final schema = json.parseField<String>('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<String, dynamic> additionalFields = <String, dynamic>{};
final Map<String, dynamic> additionalFields;

@override
bool operator ==(Object other) {
Expand Down
81 changes: 81 additions & 0 deletions lib/src/definitions/context_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,96 @@
//
// 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 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<String, dynamic>) {
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<String, String> '
'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<ContextEntry> parseContext(
dynamic json,
PrefixMapping prefixMapping,
) {
var firstEntry = true;

if (json is String) {
return [
ContextEntry.fromJson(json, prefixMapping, firstEntry: firstEntry)
];
}

if (json is List<dynamic>) {
final List<ContextEntry> 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;

Expand Down
35 changes: 13 additions & 22 deletions lib/src/definitions/data_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic> json) {
// TODO(JKRhb): Parse more DataSchema values
final Object? atType = json['@type'];
if (atType is String) {
dataSchema.atType = [atType];
} else if (atType is List<String>) {
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<String>('@type')
..title = json.parseField<String>('title')
..titles = json.parseMapField<String>('titles')
..description = json.parseField<String>('description')
..constant = json.parseField<Object>('constant')
..enumeration = json.parseField<List<Object>>('enum')
..readOnly = json.parseField<bool>('readOnly') ?? dataSchema.readOnly
..writeOnly = json.parseField<bool>('writeOnly') ?? dataSchema.writeOnly
..format = json.parseField<String>('format')
..type = json.parseField<String>('type');
}

/// Metadata that describes the data format used. It can be used for validation.
Expand Down
40 changes: 23 additions & 17 deletions lib/src/definitions/expected_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, dynamic>? additionalFields})
: additionalFields = Map.fromEntries(
additionalFields?.entries
.where((element) => element.key != 'contentType') ??
[],
);

/// Creates an [ExpectedResponse] from a [json] object.
ExpectedResponse.fromJson(Map<String, dynamic> json)
: contentType = _parseContentType(json['contentType']) {
for (final entry in json.entries) {
if (entry.key == 'response') {
continue;
}
static ExpectedResponse? fromJson(
Map<String, dynamic> json, [
Set<String>? parsedFields,
]) {
final responseJson = json['response'];
parsedFields?.add('response');

additionalFields[entry.key] = entry.value;
if (responseJson is! Map<String, dynamic>) {
return null;
}

return ExpectedResponse(
responseJson.parseRequiredField<String>('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<String, dynamic> additionalFields = <String, dynamic>{};

static String _parseContentType(dynamic contentType) {
if (contentType is! String) {
throw ValidationException('contentType of response map is not a String!');
}
return contentType;
}
final Map<String, dynamic> additionalFields;
}
87 changes: 87 additions & 0 deletions lib/src/definitions/extensions/json_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import '../validation/validation_exception.dart';

/// Extension for parsing fields of JSON objects.
extension ParseField on Map<String, dynamic> {
/// 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<T>(String name, [Set<String>? 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<T>(String name, [Set<String>? 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<String, T>? parseMapField<T>(String name, [Set<String>? parsedFields]) {
final dynamic mapField = this[name];
parsedFields?.add(name);

if (mapField is Map<String, dynamic>) {
final Map<String, T> 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<T>` 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<T>? parseArrayField<T>(String name, [Set<String>? parsedFields]) {
final value = this[name];
parsedFields?.add(name);

if (value is T) {
return [value];
} else if (value is List<dynamic>) {
return value.whereType<T>().toList(growable: false);
}

return null;
}
}
Loading

0 comments on commit 506d20b

Please sign in to comment.