blob: aabf3570c57228e553ead3479c8b26de09e186de [file] [log] [blame]
// Copyright (c) 2019, 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.
import 'package:json_annotation/json_annotation.dart';
import 'package:yaml/yaml.dart';
/// Decodes [yamlContent] as YAML and calls [constructor] with the resulting
/// [Map].
///
/// If there are errors thrown while decoding [yamlContent], if it is not a
/// [Map] or if [CheckedFromJsonException] is thrown when calling [constructor],
/// a [ParsedYamlException] will be thrown.
///
/// If [sourceUrl] is passed, it's used as the URL from which the YAML
/// originated for error reporting. It can be a [String], a [Uri], or `null`.
///
/// If [allowNull] is `true`, a `null` value from [yamlContent] will be allowed
/// and passed to [constructor]. [constructor], therefore, will need to handle
/// `null` values.
T checkedYamlDecode<T>(
String yamlContent,
T Function(Map) constructor, {
sourceUrl,
bool allowNull = false,
}) {
allowNull ??= false;
YamlNode yaml;
try {
yaml = loadYamlNode(yamlContent, sourceUrl: sourceUrl);
} on YamlException catch (e) {
throw ParsedYamlException.fromYamlException(e);
}
Map map;
if (yaml is YamlMap) {
map = yaml;
} else if (allowNull && yaml is YamlScalar && yaml.value == null) {
// TODO(kevmoo): test this case!
map = null;
} else {
throw ParsedYamlException('Not a map', yaml);
}
try {
return constructor(map);
} on CheckedFromJsonException catch (e) {
throw toParsedYamlException(e);
}
}
/// Returns a [ParsedYamlException] for the provided [exception].
///
/// This function assumes `exception.map` is of type `YamlMap` from
/// `package:yaml`. If not, you may provide an alternative via [exceptionMap].
ParsedYamlException toParsedYamlException(
CheckedFromJsonException exception, {
YamlMap exceptionMap,
}) {
final yamlMap = exceptionMap ?? exception.map as YamlMap;
final innerError = exception.innerError;
if (exception.badKey) {
final key = (innerError is UnrecognizedKeysException)
? innerError.unrecognizedKeys.first
: exception.key;
final node = yamlMap.nodes.keys.singleWhere(
(k) => (k as YamlScalar).value == key,
orElse: () => yamlMap) as YamlNode;
return ParsedYamlException(
exception.message,
node,
innerError: exception,
);
} else {
final yamlValue = yamlMap.nodes[exception.key];
if (yamlValue == null) {
// TODO(kevmoo): test this case!
return ParsedYamlException(
exception.message,
yamlMap,
innerError: exception,
);
} else {
var message = 'Unsupported value for "${exception.key}".';
if (exception.message != null) {
message = '$message ${exception.message}';
}
return ParsedYamlException(
message,
yamlValue,
innerError: exception,
);
}
}
}
/// An exception thrown when parsing YAML that contains information about the
/// location in the source where the exception occurred.
class ParsedYamlException implements Exception {
/// Describes the nature of the parse failure.
final String message;
/// The node associated with this exception.
///
/// May be `null` if there was an error decoding.
final YamlNode yamlNode;
/// If this exception was thrown as a result of another error,
/// contains the source error object.
final Object innerError;
ParsedYamlException(
this.message,
this.yamlNode, {
this.innerError,
}) : assert(message != null),
assert(yamlNode != null);
factory ParsedYamlException.fromYamlException(YamlException exception) =>
_WrappedYamlException(exception);
/// Returns [message] formatted with source information provided by
/// [yamlNode].
String get formattedMessage => yamlNode.span.message(message);
@override
String toString() => 'ParsedYamlException: $formattedMessage';
}
class _WrappedYamlException implements ParsedYamlException {
_WrappedYamlException(this.innerError);
@override
String get formattedMessage => innerError.span.message(innerError.message);
@override
final YamlException innerError;
@override
String get message => innerError.message;
@override
YamlNode get yamlNode => null;
@override
String toString() => 'ParsedYamlException: $formattedMessage';
}