// 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 { 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 { 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 { final Set? 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 { 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` 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 extends Value> { final Value elementReader; final bool supportSingleton; const ListValue(this.elementReader, {this.supportSingleton = false}); @override List read(dynamic value, {String context = ''}) { if (value is List) { List 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` value encoded as a map. /// /// The individual map entry values of type [V] are read using the /// [valueReader]. class MapValue extends Value> { final Value valueReader; const MapValue(this.valueReader); @override Map read(dynamic value, {String context = ''}) { if (value is Map) { Map 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 extends Value { final List 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 extends Value { 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 { /// The name of the property as found in property map. final String name; /// The reader used to decode values of this property. final Value 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? 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': "); } } }