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 }