// 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;
}
