Newer
Older
dub_jkp / source / dub / internal / configy / Exceptions.d
  1. /*******************************************************************************
  2.  
  3. Definitions for Exceptions used by the config module.
  4.  
  5. Copyright:
  6. Copyright (c) 2019-2022 BOSAGORA Foundation
  7. All rights reserved.
  8.  
  9. License:
  10. MIT License. See LICENSE for details.
  11.  
  12. *******************************************************************************/
  13.  
  14. module dub.internal.configy.Exceptions;
  15.  
  16. import dub.internal.configy.Utils;
  17.  
  18. import dub.internal.dyaml.exception;
  19. import dub.internal.dyaml.node;
  20.  
  21. import std.algorithm : filter, map;
  22. import std.format;
  23. import std.string : soundexer;
  24.  
  25. /*******************************************************************************
  26.  
  27. Base exception type thrown by the config parser
  28.  
  29. Whenever dealing with Exceptions thrown by the config parser, catching
  30. this type will allow to optionally format with colors:
  31. ```
  32. try
  33. {
  34. auto conf = parseConfigFile!Config(cmdln);
  35. // ...
  36. }
  37. catch (ConfigException exc)
  38. {
  39. writeln("Parsing the config file failed:");
  40. writelfln(isOutputATTY() ? "%S" : "%s", exc);
  41. }
  42. ```
  43.  
  44. *******************************************************************************/
  45.  
  46. public abstract class ConfigException : Exception
  47. {
  48. /// Position at which the error happened
  49. public Mark yamlPosition;
  50.  
  51. /// The path at which the key resides
  52. public string path;
  53.  
  54. /// If non-empty, the key under 'path' which triggered the error
  55. /// If empty, the key should be considered part of 'path'
  56. public string key;
  57.  
  58. /// Constructor
  59. public this (string path, string key, Mark position,
  60. string file = __FILE__, size_t line = __LINE__)
  61. @safe pure nothrow @nogc
  62. {
  63. super(null, file, line);
  64. this.path = path;
  65. this.key = key;
  66. this.yamlPosition = position;
  67. }
  68.  
  69. /// Ditto
  70. public this (string path, Mark position,
  71. string file = __FILE__, size_t line = __LINE__)
  72. @safe pure nothrow @nogc
  73. {
  74. this(path, null, position, file, line);
  75. }
  76.  
  77. /***************************************************************************
  78.  
  79. Overrides `Throwable.toString` and its sink overload
  80.  
  81. It is quite likely that errors from this module may be printed directly
  82. to the end user, who might not have technical knowledge.
  83.  
  84. This format the error in a nicer format (e.g. with colors),
  85. and will additionally provide a stack-trace if the `ConfigFillerDebug`
  86. `debug` version was provided.
  87.  
  88. Format_chars:
  89. The default format char ("%s") will print a regular message.
  90. If an uppercase 's' is used ("%S"), colors will be used.
  91.  
  92. Params:
  93. sink = The sink to send the piece-meal string to
  94. spec = See https://dlang.org/phobos/std_format_spec.html
  95.  
  96. ***************************************************************************/
  97.  
  98. public override string toString () scope
  99. {
  100. // Need to be overriden otherwise the overload is shadowed
  101. return super.toString();
  102. }
  103.  
  104. /// Ditto
  105. public override void toString (scope void delegate(in char[]) sink) const scope
  106. @trusted
  107. {
  108. // This breaks the type system, as it blindly trusts a delegate
  109. // However, the type system lacks a way to sanely build an utility
  110. // which accepts a delegate with different qualifiers, so this is the
  111. // less evil approach.
  112. this.toString(cast(SinkType) sink, FormatSpec!char("%s"));
  113. }
  114.  
  115. /// Ditto
  116. public void toString (scope SinkType sink, in FormatSpec!char spec)
  117. const scope @safe
  118. {
  119. import core.internal.string : unsignedToTempString;
  120.  
  121. const useColors = spec.spec == 'S';
  122. char[20] buffer = void;
  123.  
  124. if (useColors) sink(Yellow);
  125. sink(this.yamlPosition.name);
  126. if (useColors) sink(Reset);
  127.  
  128. sink("(");
  129. if (useColors) sink(Cyan);
  130. sink(unsignedToTempString(this.yamlPosition.line, buffer));
  131. if (useColors) sink(Reset);
  132. sink(":");
  133. if (useColors) sink(Cyan);
  134. sink(unsignedToTempString(this.yamlPosition.column, buffer));
  135. if (useColors) sink(Reset);
  136. sink("): ");
  137.  
  138. if (this.path.length || this.key.length)
  139. {
  140. if (useColors) sink(Yellow);
  141. sink(this.path);
  142. if (this.path.length && this.key.length)
  143. sink(".");
  144. sink(this.key);
  145. if (useColors) sink(Reset);
  146. sink(": ");
  147. }
  148.  
  149. this.formatMessage(sink, spec);
  150.  
  151. debug (ConfigFillerDebug)
  152. {
  153. sink("\n\tError originated from: ");
  154. sink(this.file);
  155. sink("(");
  156. sink(unsignedToTempString(line, buffer));
  157. sink(")");
  158.  
  159. if (!this.info)
  160. return;
  161.  
  162. () @trusted nothrow
  163. {
  164. try
  165. {
  166. sink("\n----------------");
  167. foreach (t; info)
  168. {
  169. sink("\n"); sink(t);
  170. }
  171. }
  172. // ignore more errors
  173. catch (Throwable) {}
  174. }();
  175. }
  176. }
  177.  
  178. /// Hook called by `toString` to simplify coloring
  179. protected abstract void formatMessage (
  180. scope SinkType sink, in FormatSpec!char spec)
  181. const scope @safe;
  182. }
  183.  
  184. /// A configuration exception that is only a single message
  185. package final class ConfigExceptionImpl : ConfigException
  186. {
  187. public this (string msg, Mark position,
  188. string file = __FILE__, size_t line = __LINE__)
  189. @safe pure nothrow @nogc
  190. {
  191. this(msg, null, null, position, file, line);
  192. }
  193.  
  194. public this (string msg, string path, string key, Mark position,
  195. string file = __FILE__, size_t line = __LINE__)
  196. @safe pure nothrow @nogc
  197. {
  198. super(path, key, position, file, line);
  199. this.msg = msg;
  200. }
  201.  
  202. protected override void formatMessage (
  203. scope SinkType sink, in FormatSpec!char spec)
  204. const scope @safe
  205. {
  206. sink(this.msg);
  207. }
  208. }
  209.  
  210. /// Exception thrown when the type of the YAML node does not match the D type
  211. package final class TypeConfigException : ConfigException
  212. {
  213. /// The actual (in the YAML document) type of the node
  214. public string actual;
  215.  
  216. /// The expected (as specified in the D type) type
  217. public string expected;
  218.  
  219. /// Constructor
  220. public this (Node node, string expected, string path, string key = null,
  221. string file = __FILE__, size_t line = __LINE__)
  222. @safe nothrow
  223. {
  224. this(node.nodeTypeString(), expected, path, key, node.startMark(),
  225. file, line);
  226. }
  227.  
  228. /// Ditto
  229. public this (string actual, string expected, string path, string key,
  230. Mark position, string file = __FILE__, size_t line = __LINE__)
  231. @safe pure nothrow @nogc
  232. {
  233. super(path, key, position, file, line);
  234. this.actual = actual;
  235. this.expected = expected;
  236. }
  237.  
  238. /// Format the message with or without colors
  239. protected override void formatMessage (
  240. scope SinkType sink, in FormatSpec!char spec)
  241. const scope @safe
  242. {
  243. const useColors = spec.spec == 'S';
  244.  
  245. const fmt = "Expected to be of type %s, but is a %s";
  246.  
  247. if (useColors)
  248. formattedWrite(sink, fmt, this.expected.paint(Green), this.actual.paint(Red));
  249. else
  250. formattedWrite(sink, fmt, this.expected, this.actual);
  251. }
  252. }
  253.  
  254. /// Similar to a `TypeConfigException`, but specific to `Duration`
  255. package final class DurationTypeConfigException : ConfigException
  256. {
  257. /// The list of valid fields
  258. public immutable string[] DurationSuffixes = [
  259. "weeks", "days", "hours", "minutes", "seconds",
  260. "msecs", "usecs", "hnsecs", "nsecs",
  261. ];
  262.  
  263. /// Actual type of the node
  264. public string actual;
  265.  
  266. /// Constructor
  267. public this (Node node, string path, string file = __FILE__, size_t line = __LINE__)
  268. @safe nothrow
  269. {
  270. super(path, null, node.startMark(), file, line);
  271. this.actual = node.nodeTypeString();
  272. }
  273.  
  274. /// Format the message with or without colors
  275. protected override void formatMessage (
  276. scope SinkType sink, in FormatSpec!char spec)
  277. const scope @safe
  278. {
  279. const useColors = spec.spec == 'S';
  280.  
  281. const fmt = "Field is of type %s, but expected a mapping with at least one of: %-(%s, %)";
  282. if (useColors)
  283. formattedWrite(sink, fmt, this.actual.paint(Red),
  284. this.DurationSuffixes.map!(s => s.paint(Green)));
  285. else
  286. formattedWrite(sink, fmt, this.actual, this.DurationSuffixes);
  287. }
  288. }
  289.  
  290. /// Exception thrown when an unknown key is found in strict mode
  291. public class UnknownKeyConfigException : ConfigException
  292. {
  293. /// The list of valid field names
  294. public immutable string[] fieldNames;
  295.  
  296. /// Constructor
  297. public this (string path, string key, immutable string[] fieldNames,
  298. Mark position, string file = __FILE__, size_t line = __LINE__)
  299. @safe pure nothrow @nogc
  300. {
  301. super(path, key, position, file, line);
  302. this.fieldNames = fieldNames;
  303. }
  304.  
  305. /// Format the message with or without colors
  306. protected override void formatMessage (
  307. scope SinkType sink, in FormatSpec!char spec)
  308. const scope @safe
  309. {
  310. const useColors = spec.spec == 'S';
  311.  
  312. // Try to find a close match, as the error is likely a typo
  313. // This is especially important when the config file has a large
  314. // number of fields, where the message is otherwise near-useless.
  315. const origSound = soundexer(this.key);
  316. auto matches = this.fieldNames.filter!(f => f.soundexer == origSound);
  317. const hasMatch = !matches.save.empty;
  318.  
  319. if (hasMatch)
  320. {
  321. const fmt = "Key is not a valid member of this section. Did you mean: %-(%s, %)";
  322. if (useColors)
  323. formattedWrite(sink, fmt, matches.map!(f => f.paint(Green)));
  324. else
  325. formattedWrite(sink, fmt, matches);
  326. }
  327. else
  328. {
  329. // No match, just print everything
  330. const fmt = "Key is not a valid member of this section. There are %s valid keys: %-(%s, %)";
  331. if (useColors)
  332. formattedWrite(sink, fmt, this.fieldNames.length.paint(Yellow),
  333. this.fieldNames.map!(f => f.paint(Green)));
  334. else
  335. formattedWrite(sink, fmt, this.fieldNames.length, this.fieldNames);
  336. }
  337. }
  338. }
  339.  
  340. /// Exception thrown when a required key is missing
  341. public class MissingKeyException : ConfigException
  342. {
  343. /// Constructor
  344. public this (string path, string key, Mark position,
  345. string file = __FILE__, size_t line = __LINE__)
  346. @safe pure nothrow @nogc
  347. {
  348. super(path, key, position, file, line);
  349. }
  350.  
  351. /// Format the message with or without colors
  352. protected override void formatMessage (
  353. scope SinkType sink, in FormatSpec!char spec)
  354. const scope @safe
  355. {
  356. sink("Required key was not found in configuration or command line arguments");
  357. }
  358. }
  359.  
  360. /// Wrap an user-thrown Exception that happened in a Converter/ctor/fromString
  361. public class ConstructionException : ConfigException
  362. {
  363. /// Constructor
  364. public this (Exception next, string path, Mark position,
  365. string file = __FILE__, size_t line = __LINE__)
  366. @safe pure nothrow @nogc
  367. {
  368. super(path, position, file, line);
  369. this.next = next;
  370. }
  371.  
  372. /// Format the message with or without colors
  373. protected override void formatMessage (
  374. scope SinkType sink, in FormatSpec!char spec)
  375. const scope @trusted
  376. {
  377. if (auto dyn = cast(ConfigException) this.next)
  378. dyn.toString(sink, spec);
  379. else
  380. sink(this.next.message);
  381. }
  382. }