blob: a28b31b3345a370063cd39d77486d6088b3c94b5 [file] [log] [blame]
// Copyright (c) 2023, 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.
/// Utilities for loading and validating YAML from a config file.
///
/// When writing YAML config files it's often preferable to force a few
/// constraints on the YAML files. Specifically:
/// * Root value must be a map.
/// * No anchors / aliases, this can't be represented in JSON and
/// can be used to created cycles, which might crash someone trying to
/// serialize the data as JSON.
/// * Maps can only have string keys.
///
/// Basically, only allow value that can't be represented in JSON.
///
/// This library aims to provide a [parseYamlFromConfigFile] that throws
/// a [FormatException] when parsing YAML files that doesn't satisfy this
/// constraint.
library;
import 'package:yaml/yaml.dart'
show YamlList, YamlMap, YamlNode, YamlScalar, loadYamlNode;
/// Parse YAML from a config file.
///
/// This will validate that:
/// * Maps only have string keys.
/// * Anchor and aliases are not used (to avoid cycles).
/// * Values can be represented as JSON.
/// * Root node is a map.
///
/// Throw will throw [FormatException], if the [yamlString] does not satisfy
/// constraints above.
///
/// Returns a structure consisting of the following types:
/// * `null`,
/// * [bool] (`true` or `false`),
/// * [String],
/// * [num] ([int] or [double]),
/// * `List<Object?>`, and,
/// * `Map<String, Object?>`.
Map<String, Object?> parseYamlFromConfigFile(String yamlString) {
final visited = <YamlNode>{};
Object? toPlainType(YamlNode n) {
if (!visited.add(n)) {
throw const FormatException(
'Anchors/aliases are not supported in YAML config files',
);
}
if (n is YamlScalar) {
return switch (n.value) {
null => null,
String s => s,
num n => n,
bool b => b,
_ => throw const FormatException(
'Only null, string, number, bool, map and lists are supported '
'in YAML config files',
),
};
}
if (n is YamlList) {
return n.nodes.map(toPlainType).toList();
}
if (n is YamlMap) {
return n.nodes.map((key, value) {
final k = toPlainType(key as YamlNode);
if (k is! String) {
throw const FormatException(
'Only string keys are allowed in YAML config files',
);
}
return MapEntry(k, toPlainType(value));
});
}
throw UnsupportedError('Unknown YamlNode: $n');
}
final value = toPlainType(loadYamlNode(yamlString));
if (value is! Map<String, Object?>) {
throw const FormatException('The root of a YAML config file must be a map');
}
return value;
}