blob: 3dfad279243e63e29acffa2ec176322c0f361ac2 [file] [log] [blame]
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE.md file.
/// A value of type [T] read from a yaml/json-like structure.
abstract class Value<T> {
const Value();
/// Reads the [T] value from [value].
///
/// If the [value] isn't a valid encoding of [T] an [ArgumentError] is thrown
/// using [context] to provide additional information in the error message.
T read(dynamic value, {String context = ''});
}
/// A `bool` value encoded as a bool.
class BoolValue extends Value<bool> {
const BoolValue();
@override
bool read(dynamic value, {String context = ''}) {
if (value is bool) {
return value;
} else {
throw new ArgumentError("${context}Value must be a boolean.");
}
}
}
/// A `String` value encoded as a string.
///
/// If [options] is provided, only values in [options] are allowed.
class StringValue extends Value<String> {
final Set<String>? options;
const StringValue({this.options});
@override
String read(dynamic value, {String context = ''}) {
if (value is String) {
if (options != null && !options!.contains(value)) {
throw new ArgumentError("${context}Unexpected value '${value}'. "
"Expected one of: ${options!.join(',')}.");
}
return value;
} else {
throw new ArgumentError("${context}Value must be a string.");
}
}
}
/// An `int` value encoded as an integer.
class IntValue extends Value<int> {
const IntValue();
@override
int read(dynamic value, {String context = ''}) {
if (value is int) {
return value;
} else {
throw new ArgumentError("${context}Value must be a integer.");
}
}
}
/// A `List<E>` value encoded as a list or a single element, if
/// [supportSingleton] is `true`.
///
/// The individual elements of type [E] are read using the [elementReader].
class ListValue<E> extends Value<List<E>> {
final Value<E> elementReader;
final bool supportSingleton;
const ListValue(this.elementReader, {this.supportSingleton: false});
@override
List<E> read(dynamic value, {String context = ''}) {
if (value is List) {
List<E> list = [];
for (dynamic element in value) {
list.add(elementReader.read(element));
}
return list;
} else if (supportSingleton) {
return [elementReader.read(value, context: context)];
} else {
throw new ArgumentError("${context}Value must be a list.");
}
}
}
/// A `Map<String, V>` value encoded as a map.
///
/// The individual map entry values of type [V] are read using the
/// [valueReader].
class MapValue<V> extends Value<Map<String, V>> {
final Value<V> valueReader;
const MapValue(this.valueReader);
@override
Map<String, V> read(dynamic value, {String context = ''}) {
if (value is Map) {
Map<String, V> map = {};
value.forEach((key, value) {
map[key as String] = valueReader.read(value, context: context);
});
return map;
} else {
throw new ArgumentError("${context}Value must be a map.");
}
}
}
/// An enum value of type [E] encoding as a string using the enum value name.
class EnumValue<E extends Enum> extends Value<E> {
final List<E> enumValues;
const EnumValue(this.enumValues);
@override
E read(dynamic value, {String context = ''}) {
if (value is String) {
for (E enumValue in enumValues) {
if (value == enumValue.name) {
return enumValue;
}
}
throw new ArgumentError("${context}Unexpected value: '${value}'. "
"Expected one of: ${enumValues.map((e) => e.name).join(',')}.");
} else {
throw new ArgumentError("${context}Value must be a string.");
}
}
}
/// A value of type [O] encoded as a value of type [I], using [valueReader] to
/// decode the value.
class CustomValue<I, O> extends Value<O> {
final O Function(I) valueReader;
const CustomValue(this.valueReader);
@override
O read(dynamic value, {String context = ''}) {
if (value is I) {
return valueReader(value);
} else {
throw new ArgumentError("${context}Value must be of type $I.");
}
}
}
/// A potentially optional named property of type [T].
///
/// This class can be used to document and support the decoding of properties
/// defined in a yaml/json-like map.
class Property<T> {
/// The name of the property as found in property map.
final String name;
/// The reader used to decode values of this property.
final Value<T> valueReader;
/// The default value for this property if omitted.
final T? defaultValue;
/// Whether this property is required.
final bool required;
/// Creates a required property with given [name] that uses [valueReader] to
/// decode its value.
const Property.required(this.name, this.valueReader)
: required = true,
defaultValue = null,
assert(null is! T);
/// Creates an optional property with given [name] that uses [valueReader] to
/// decode its value when present and [defaultValue] if omitted.
const Property.optional(this.name, this.valueReader, {this.defaultValue})
: required = false,
assert(defaultValue is T);
/// Reads this property from [map].
///
/// If [keys] is provided, [name] is removed from [keys] to signal that this
/// property has been recognized. This can be used to detect and report
/// unknown/unsupported properties.
T read(Map map, [Set<String>? keys]) {
dynamic value = map[name];
keys?.remove(name);
if (value == null) {
if (required) {
throw new ArgumentError("Missing property '$name'.");
}
return defaultValue as T;
} else {
return valueReader.read(value, context: "Property '$name': ");
}
}
}