/******************************************************************************* Definitions for Exceptions used by the config module. Copyright: Copyright (c) 2019-2022 BOSAGORA Foundation All rights reserved. License: MIT License. See LICENSE for details. *******************************************************************************/ module configy.Exceptions; import configy.Utils; import dyaml.exception; import dyaml.node; import std.algorithm : map; import std.format; /******************************************************************************* Base exception type thrown by the config parser Whenever dealing with Exceptions thrown by the config parser, catching this type will allow to optionally format with colors: ``` try { auto conf = parseConfigFile!Config(cmdln); // ... } catch (ConfigException exc) { writeln("Parsing the config file failed:"); writelfln(isOutputATTY() ? "%S" : "%s", exc); } ``` *******************************************************************************/ public abstract class ConfigException : Exception { /// Position at which the error happened public Mark yamlPosition; /// The path at which the key resides public string path; /// If non-empty, the key under 'path' which triggered the error /// If empty, the key should be considered part of 'path' public string key; /// Constructor public this (string path, string key, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { super(null, file, line); this.path = path; this.key = key; this.yamlPosition = position; } /// Ditto public this (string path, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { this(path, null, position, file, line); } /*************************************************************************** Overrides `Throwable.toString` and its sink overload It is quite likely that errors from this module may be printed directly to the end user, who might not have technical knowledge. This format the error in a nicer format (e.g. with colors), and will additionally provide a stack-trace if the `ConfigFillerDebug` `debug` version was provided. Format_chars: The default format char ("%s") will print a regular message. If an uppercase 's' is used ("%S"), colors will be used. Params: sink = The sink to send the piece-meal string to spec = See https://dlang.org/phobos/std_format_spec.html ***************************************************************************/ public override string toString () scope { // Need to be overriden otherwise the overload is shadowed return super.toString(); } /// Ditto public override void toString (scope void delegate(in char[]) sink) const scope @trusted { // This breaks the type system, as it blindly trusts a delegate // However, the type system lacks a way to sanely build an utility // which accepts a delegate with different qualifiers, so this is the // less evil approach. this.toString(cast(SinkType) sink, FormatSpec!char("%s")); } /// Ditto public void toString (scope SinkType sink, in FormatSpec!char spec) const scope @safe { import core.internal.string : unsignedToTempString; const useColors = spec.spec == 'S'; char[20] buffer = void; if (useColors) sink(Yellow); sink(this.yamlPosition.name); if (useColors) sink(Reset); sink("("); if (useColors) sink(Cyan); sink(unsignedToTempString(this.yamlPosition.line, buffer)); if (useColors) sink(Reset); sink(":"); if (useColors) sink(Cyan); sink(unsignedToTempString(this.yamlPosition.column, buffer)); if (useColors) sink(Reset); sink("): "); if (this.path.length || this.key.length) { if (useColors) sink(Yellow); sink(this.path); if (this.path.length && this.key.length) sink("."); sink(this.key); if (useColors) sink(Reset); sink(": "); } this.formatMessage(sink, spec); debug (ConfigFillerDebug) { sink("\n\tError originated from: "); sink(this.file); sink("("); sink(unsignedToTempString(line, buffer)); sink(")"); if (!this.info) return; () @trusted nothrow { try { sink("\n----------------"); foreach (t; info) { sink("\n"); sink(t); } } // ignore more errors catch (Throwable) {} }(); } } /// Hook called by `toString` to simplify coloring protected abstract void formatMessage ( scope SinkType sink, in FormatSpec!char spec) const scope @safe; } /// A configuration exception that is only a single message package final class ConfigExceptionImpl : ConfigException { public this (string msg, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { this(msg, null, null, position, file, line); } public this (string msg, string path, string key, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { super(path, key, position, file, line); this.msg = msg; } protected override void formatMessage ( scope SinkType sink, in FormatSpec!char spec) const scope @safe { sink(this.msg); } } /// Exception thrown when the type of the YAML node does not match the D type package final class TypeConfigException : ConfigException { /// The actual (in the YAML document) type of the node public string actual; /// The expected (as specified in the D type) type public string expected; /// Constructor public this (Node node, string expected, string path, string key = null, string file = __FILE__, size_t line = __LINE__) @safe nothrow { this(node.nodeTypeString(), expected, path, key, node.startMark(), file, line); } /// Ditto public this (string actual, string expected, string path, string key, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { super(path, key, position, file, line); this.actual = actual; this.expected = expected; } /// Format the message with or without colors protected override void formatMessage ( scope SinkType sink, in FormatSpec!char spec) const scope @safe { const useColors = spec.spec == 'S'; const fmt = "Expected to be of type %s, but is a %s"; if (useColors) formattedWrite(sink, fmt, this.expected.paint(Green), this.actual.paint(Red)); else formattedWrite(sink, fmt, this.expected, this.actual); } } /// Similar to a `TypeConfigException`, but specific to `Duration` package final class DurationTypeConfigException : ConfigException { /// The list of valid field (a manifest constant, but we want to avoid the dependency) public immutable string[] DurationSuffixes; /// Actual type of the node public string actual; /// Constructor public this (Node node, string path, immutable string[] DurationSuffixes, string file = __FILE__, size_t line = __LINE__) @safe nothrow { super(path, null, node.startMark(), file, line); this.actual = node.nodeTypeString(); this.DurationSuffixes = DurationSuffixes; } /// Format the message with or without colors protected override void formatMessage ( scope SinkType sink, in FormatSpec!char spec) const scope @safe { const useColors = spec.spec == 'S'; const fmt = "Field is of type %s, but expected a mapping with at least one of: %-(%s, %)"; if (useColors) formattedWrite(sink, fmt, this.actual.paint(Red), this.DurationSuffixes.map!(s => s[1 .. $].paint(Green))); else formattedWrite(sink, fmt, this.actual, this.DurationSuffixes.map!(s => s[1 .. $])); } } /// Exception thrown when an unknown key is found in strict mode public class UnknownKeyConfigException : ConfigException { /// The list of valid field names public immutable string[] fieldNames; /// Constructor public this (string path, string key, immutable string[] fieldNames, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { super(path, key, position, file, line); this.fieldNames = fieldNames; } /// Format the message with or without colors protected override void formatMessage ( scope SinkType sink, in FormatSpec!char spec) const scope @safe { const useColors = spec.spec == 'S'; const fmt = "Key is not a valid member of this section. There are %s valid keys: %-(%s, %)"; if (useColors) formattedWrite(sink, fmt, this.fieldNames.length.paint(Yellow), this.fieldNames.map!(f => f.paint(Green))); else formattedWrite(sink, fmt, this.fieldNames.length, this.fieldNames); } } /// Exception thrown when a required key is missing public class MissingKeyException : ConfigException { /// Constructor public this (string path, string key, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { super(path, key, position, file, line); } /// Format the message with or without colors protected override void formatMessage ( scope SinkType sink, in FormatSpec!char spec) const scope @safe { sink("Required key was not found in configuration or command line arguments"); } } /// Wrap an user-thrown Exception that happened in a Converter/ctor/fromString public class ConstructionException : ConfigException { /// Constructor public this (Exception next, string path, Mark position, string file = __FILE__, size_t line = __LINE__) @safe pure nothrow @nogc { super(path, position, file, line); this.next = next; } /// Format the message with or without colors protected override void formatMessage ( scope SinkType sink, in FormatSpec!char spec) const scope @trusted { // Here we break the type system too, calling a `@system` function, // but since it takes no parameter, why would it not be `@safe` ? sink(this.next.message()); } }