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 configy.Exceptions;
15 
16 import configy.Utils;
17 
18 import dyaml.exception;
19 import 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 }