| // 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': "); |
| } |
| } |
| } |