| // 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 file. | |
| /// A value of type [T] read from a yaml/json-like structure. | |
| abstract class Value<T> { | |
| const new(); | |
| /// 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 new(); | |
| @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 new({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 new(); | |
| @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 new(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 new(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 new(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 new(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 new 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 new 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': "); | |
| } | |
| } | |
| } |